From 3438a4f063f5a4d04b824531ca4e07c1700bfa4e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 26 May 2025 20:31:18 +0000 Subject: [PATCH 001/266] Bump version to 2025.6.0b0 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5b299fd0187..a36aa88ca84 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 = 6 -PATCH_VERSION: Final = "0.dev0" +PATCH_VERSION: Final = "0b0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index 1fc4a28b9da..6a1fa4d49b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.6.0.dev0" +version = "2025.6.0b0" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From 9483a88ee1fda9f0dfc3c1dca56022432913aff0 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 26 May 2025 23:47:53 +0200 Subject: [PATCH 002/266] Fix translation for sensor measurement angle state class (#145649) --- homeassistant/components/sensor/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/strings.json b/homeassistant/components/sensor/strings.json index 2268d2797e4..ecaeb2504d9 100644 --- a/homeassistant/components/sensor/strings.json +++ b/homeassistant/components/sensor/strings.json @@ -135,7 +135,7 @@ "name": "State class", "state": { "measurement": "Measurement", - "measurement_angle": "Measurement Angle", + "measurement_angle": "Measurement angle", "total": "Total", "total_increasing": "Total increasing" } From 77031d1ae4235c13a7a8e768a954cb84ae0bce44 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 26 May 2025 23:08:07 +0200 Subject: [PATCH 003/266] Fix Aquacell snapshot (#145651) --- tests/components/aquacell/snapshots/test_sensor.ambr | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/components/aquacell/snapshots/test_sensor.ambr b/tests/components/aquacell/snapshots/test_sensor.ambr index f512b2a824d..c24a7f43cfe 100644 --- a/tests/components/aquacell/snapshots/test_sensor.ambr +++ b/tests/components/aquacell/snapshots/test_sensor.ambr @@ -77,6 +77,7 @@ 'original_name': 'Last update', 'platform': 'aquacell', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_update', 'unique_id': 'DSN-last_update', From f60de45b52987c34f3c2de193b37e6be556be363 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 27 May 2025 08:49:25 +0200 Subject: [PATCH 004/266] Fix Amazon devices offline handling (#145656) --- .../components/amazon_devices/entity.py | 6 ++- .../amazon_devices/test_binary_sensor.py | 32 +++++++++++++++ .../components/amazon_devices/test_notify.py | 37 +++++++++++++++++- .../components/amazon_devices/test_switch.py | 39 ++++++++++++++++++- 4 files changed, 110 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/amazon_devices/entity.py b/homeassistant/components/amazon_devices/entity.py index 825a63db476..bab8009ceb0 100644 --- a/homeassistant/components/amazon_devices/entity.py +++ b/homeassistant/components/amazon_devices/entity.py @@ -50,4 +50,8 @@ class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]): @property def available(self) -> bool: """Return True if entity is available.""" - return super().available and self._serial_num in self.coordinator.data + return ( + super().available + and self._serial_num in self.coordinator.data + and self.device.online + ) diff --git a/tests/components/amazon_devices/test_binary_sensor.py b/tests/components/amazon_devices/test_binary_sensor.py index bbe8af17a8e..b31d85e06aa 100644 --- a/tests/components/amazon_devices/test_binary_sensor.py +++ b/tests/components/amazon_devices/test_binary_sensor.py @@ -17,6 +17,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from . import setup_integration +from .const import TEST_SERIAL_NUMBER from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform @@ -69,3 +70,34 @@ async def test_coordinator_data_update_fails( assert (state := hass.states.get(entity_id)) assert state.state == STATE_UNAVAILABLE + + +async def test_offline_device( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test offline device handling.""" + + entity_id = "binary_sensor.echo_test_connectivity" + + mock_amazon_devices_client.get_devices_data.return_value[ + TEST_SERIAL_NUMBER + ].online = False + + await setup_integration(hass, mock_config_entry) + + assert (state := hass.states.get(entity_id)) + assert state.state == STATE_UNAVAILABLE + + mock_amazon_devices_client.get_devices_data.return_value[ + TEST_SERIAL_NUMBER + ].online = True + + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert (state := hass.states.get(entity_id)) + assert state.state != STATE_UNAVAILABLE diff --git a/tests/components/amazon_devices/test_notify.py b/tests/components/amazon_devices/test_notify.py index c1147af94c7..b486380fd07 100644 --- a/tests/components/amazon_devices/test_notify.py +++ b/tests/components/amazon_devices/test_notify.py @@ -6,19 +6,21 @@ from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion +from homeassistant.components.amazon_devices.coordinator import SCAN_INTERVAL from homeassistant.components.notify import ( ATTR_MESSAGE, DOMAIN as NOTIFY_DOMAIN, SERVICE_SEND_MESSAGE, ) -from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.util import dt as dt_util from . import setup_integration +from .const import TEST_SERIAL_NUMBER -from tests.common import MockConfigEntry, snapshot_platform +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform @pytest.mark.usefixtures("entity_registry_enabled_by_default") @@ -68,3 +70,34 @@ async def test_notify_send_message( assert (state := hass.states.get(entity_id)) assert state.state == now.isoformat() + + +async def test_offline_device( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test offline device handling.""" + + entity_id = "notify.echo_test_announce" + + mock_amazon_devices_client.get_devices_data.return_value[ + TEST_SERIAL_NUMBER + ].online = False + + await setup_integration(hass, mock_config_entry) + + assert (state := hass.states.get(entity_id)) + assert state.state == STATE_UNAVAILABLE + + mock_amazon_devices_client.get_devices_data.return_value[ + TEST_SERIAL_NUMBER + ].online = True + + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert (state := hass.states.get(entity_id)) + assert state.state != STATE_UNAVAILABLE diff --git a/tests/components/amazon_devices/test_switch.py b/tests/components/amazon_devices/test_switch.py index 004d6cce842..24af96db280 100644 --- a/tests/components/amazon_devices/test_switch.py +++ b/tests/components/amazon_devices/test_switch.py @@ -12,7 +12,13 @@ from homeassistant.components.switch import ( SERVICE_TURN_OFF, SERVICE_TURN_ON, ) -from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, Platform +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, + Platform, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -89,3 +95,34 @@ async def test_switch_dnd( assert mock_amazon_devices_client.set_do_not_disturb.call_count == 2 assert (state := hass.states.get(entity_id)) assert state.state == STATE_OFF + + +async def test_offline_device( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test offline device handling.""" + + entity_id = "switch.echo_test_do_not_disturb" + + mock_amazon_devices_client.get_devices_data.return_value[ + TEST_SERIAL_NUMBER + ].online = False + + await setup_integration(hass, mock_config_entry) + + assert (state := hass.states.get(entity_id)) + assert state.state == STATE_UNAVAILABLE + + mock_amazon_devices_client.get_devices_data.return_value[ + TEST_SERIAL_NUMBER + ].online = True + + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert (state := hass.states.get(entity_id)) + assert state.state != STATE_UNAVAILABLE From 20a6a3f195f62e530c483f6b22bca806b00cd8cd Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 27 May 2025 00:01:35 +0200 Subject: [PATCH 005/266] Handle Google Nest DHCP flows (#145658) * Handle Google Nest DHCP flows * Handle Google Nest DHCP flows --- homeassistant/components/nest/config_flow.py | 1 + tests/components/nest/test_config_flow.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/homeassistant/components/nest/config_flow.py b/homeassistant/components/nest/config_flow.py index 6ed43066fe3..1513a039407 100644 --- a/homeassistant/components/nest/config_flow.py +++ b/homeassistant/components/nest/config_flow.py @@ -446,4 +446,5 @@ class NestFlowHandler( self, discovery_info: DhcpServiceInfo ) -> ConfigFlowResult: """Handle a flow initialized by discovery.""" + await self._async_handle_discovery_without_unique_id() return await self.async_step_user() diff --git a/tests/components/nest/test_config_flow.py b/tests/components/nest/test_config_flow.py index 0e6ec290841..3f369f3e127 100644 --- a/tests/components/nest/test_config_flow.py +++ b/tests/components/nest/test_config_flow.py @@ -1002,6 +1002,24 @@ async def test_dhcp_discovery( assert result.get("reason") == "missing_credentials" +@pytest.mark.parametrize( + ("nest_test_config", "sdm_managed_topic", "device_access_project_id"), + [(TEST_CONFIG_APP_CREDS, True, "project-id-2")], +) +async def test_dhcp_discovery_already_setup( + hass: HomeAssistant, oauth: OAuthFixture, setup_platform +) -> None: + """Exercise discovery dhcp with existing config entry.""" + await setup_platform() + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=FAKE_DHCP_DATA, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + @pytest.mark.parametrize(("sdm_managed_topic"), [(True)]) async def test_dhcp_discovery_with_creds( hass: HomeAssistant, From d6cadc1e3ff0c1d600a475cb4f1311f74324b3f8 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Tue, 27 May 2025 18:53:45 +0200 Subject: [PATCH 006/266] Support addresses with comma in google_travel_time (#145663) Support addresses with comma --- .../components/google_travel_time/helpers.py | 2 +- .../google_travel_time/test_helpers.py | 46 +++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 tests/components/google_travel_time/test_helpers.py diff --git a/homeassistant/components/google_travel_time/helpers.py b/homeassistant/components/google_travel_time/helpers.py index 49294455a49..2e36abd62b1 100644 --- a/homeassistant/components/google_travel_time/helpers.py +++ b/homeassistant/components/google_travel_time/helpers.py @@ -37,7 +37,7 @@ def convert_to_waypoint(hass: HomeAssistant, location: str) -> Waypoint | None: try: formatted_coordinates = coordinates.split(",") vol.Schema(cv.gps(formatted_coordinates)) - except (AttributeError, vol.ExactSequenceInvalid): + except (AttributeError, vol.Invalid): return Waypoint(address=location) return Waypoint( location=Location( diff --git a/tests/components/google_travel_time/test_helpers.py b/tests/components/google_travel_time/test_helpers.py new file mode 100644 index 00000000000..058cb214ed7 --- /dev/null +++ b/tests/components/google_travel_time/test_helpers.py @@ -0,0 +1,46 @@ +"""Tests for google_travel_time.helpers.""" + +from google.maps.routing_v2 import Location, Waypoint +from google.type import latlng_pb2 +import pytest + +from homeassistant.components.google_travel_time import helpers +from homeassistant.core import HomeAssistant + + +@pytest.mark.parametrize( + ("location", "expected_result"), + [ + ( + "12.34,56.78", + Waypoint( + location=Location( + lat_lng=latlng_pb2.LatLng( + latitude=12.34, + longitude=56.78, + ) + ) + ), + ), + ( + "12.34, 56.78", + Waypoint( + location=Location( + lat_lng=latlng_pb2.LatLng( + latitude=12.34, + longitude=56.78, + ) + ) + ), + ), + ("Some Address", Waypoint(address="Some Address")), + ("Some Street 1, 12345 City", Waypoint(address="Some Street 1, 12345 City")), + ], +) +def test_convert_to_waypoint_coordinates( + hass: HomeAssistant, location: str, expected_result: Waypoint +) -> None: + """Test convert_to_waypoint returns correct Waypoint for coordinates or address.""" + waypoint = helpers.convert_to_waypoint(hass, location) + + assert waypoint == expected_result From bfdba7713e6e6f022cbb71ef2493bfb0da85a297 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 26 May 2025 21:58:46 -0500 Subject: [PATCH 007/266] Bump aiohttp to 3.12.2 (#145671) --- 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 7da421526de..ae59ce94200 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -6,7 +6,7 @@ aiodns==3.4.0 aiohasupervisor==0.3.1 aiohttp-asyncmdnsresolver==0.1.1 aiohttp-fast-zlib==0.2.3 -aiohttp==3.12.1 +aiohttp==3.12.2 aiohttp_cors==0.7.0 aiousbwatcher==1.1.1 aiozoneinfo==0.2.3 diff --git a/pyproject.toml b/pyproject.toml index 6a1fa4d49b6..7df9230b7ee 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.1", + "aiohttp==3.12.2", "aiohttp_cors==0.7.0", "aiohttp-fast-zlib==0.2.3", "aiohttp-asyncmdnsresolver==0.1.1", diff --git a/requirements.txt b/requirements.txt index b89c164188e..e4d1cc5ba30 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ # Home Assistant Core aiodns==3.4.0 aiohasupervisor==0.3.1 -aiohttp==3.12.1 +aiohttp==3.12.2 aiohttp_cors==0.7.0 aiohttp-fast-zlib==0.2.3 aiohttp-asyncmdnsresolver==0.1.1 From f09c28e61f338d2f34f369a8daef52f640434cf5 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 27 May 2025 08:48:06 +0200 Subject: [PATCH 008/266] Fix justnimbus CI test (#145681) --- tests/components/justnimbus/test_config_flow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/justnimbus/test_config_flow.py b/tests/components/justnimbus/test_config_flow.py index 330b05bf48c..cc3a7a88285 100644 --- a/tests/components/justnimbus/test_config_flow.py +++ b/tests/components/justnimbus/test_config_flow.py @@ -1,6 +1,6 @@ """Test the JustNimbus config flow.""" -from unittest.mock import patch +from unittest.mock import MagicMock, patch from justnimbus.exceptions import InvalidClientID, JustNimbusError import pytest @@ -132,7 +132,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: with patch( "homeassistant.components.justnimbus.config_flow.justnimbus.JustNimbusClient.get_data", - return_value=True, + return_value=MagicMock(), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], From bfa919d078a5ac81c17763d8dc4c2980669d6367 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Tue, 27 May 2025 13:53:30 +0300 Subject: [PATCH 009/266] Remove confirm screen after Z-Wave usb discovery (#145682) * Remove confirm screen after Z-Wave usb discovery * Simplify async_step_usb --- .../components/zwave_js/config_flow.py | 30 +----- .../components/zwave_js/strings.json | 3 - tests/components/zwave_js/test_config_flow.py | 91 +++---------------- 3 files changed, 14 insertions(+), 110 deletions(-) diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index 3e899da0538..e2941b52522 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -170,8 +170,6 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 - _title: str - def __init__(self) -> None: """Set up flow instance.""" self.s0_legacy_key: str | None = None @@ -446,7 +444,7 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): # at least for a short time. return self.async_abort(reason="already_in_progress") if current_config_entries := self._async_current_entries(include_ignore=False): - config_entry = next( + self._reconfigure_config_entry = next( ( entry for entry in current_config_entries @@ -454,7 +452,7 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): ), None, ) - if not config_entry: + if not self._reconfigure_config_entry: return self.async_abort(reason="addon_required") vid = discovery_info.vid @@ -503,31 +501,9 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): ) title = human_name.split(" - ")[0].strip() self.context["title_placeholders"] = {CONF_NAME: title} - self._title = title - return await self.async_step_usb_confirm() - - async def async_step_usb_confirm( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Handle USB Discovery confirmation.""" - if user_input is None: - return self.async_show_form( - step_id="usb_confirm", - description_placeholders={CONF_NAME: self._title}, - ) self._usb_discovery = True - if current_config_entries := self._async_current_entries(include_ignore=False): - self._reconfigure_config_entry = next( - ( - entry - for entry in current_config_entries - if entry.data.get(CONF_USE_ADDON) - ), - None, - ) - if not self._reconfigure_config_entry: - return self.async_abort(reason="addon_required") + if current_config_entries: return await self.async_step_intent_migrate() return await self.async_step_installation_type() diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json index fbe43af1f6f..ee6efcb2fb6 100644 --- a/homeassistant/components/zwave_js/strings.json +++ b/homeassistant/components/zwave_js/strings.json @@ -98,9 +98,6 @@ "start_addon": { "title": "The Z-Wave add-on is starting." }, - "usb_confirm": { - "description": "Do you want to set up {name} with the Z-Wave add-on?" - }, "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 bae8ae55034..c9929759a49 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -585,8 +585,8 @@ async def test_abort_hassio_discovery_with_existing_flow(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_USB}, data=USB_DISCOVERY_INFO, ) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "usb_confirm" + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "installation_type" result2 = await hass.config_entries.flow.async_init( DOMAIN, @@ -664,13 +664,8 @@ async def test_usb_discovery( context={"source": config_entries.SOURCE_USB}, data=usb_discovery_info, ) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "usb_confirm" - assert result["description_placeholders"] == {"name": discovery_name} + assert mock_usb_serial_by_id.call_count == 1 - - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] is FlowResultType.MENU assert result["step_id"] == "installation_type" assert result["menu_options"] == ["intent_recommended", "intent_custom"] @@ -771,12 +766,8 @@ async def test_usb_discovery_addon_not_running( context={"source": config_entries.SOURCE_USB}, data=USB_DISCOVERY_INFO, ) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "usb_confirm" + assert mock_usb_serial_by_id.call_count == 2 - - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] is FlowResultType.MENU assert result["step_id"] == "installation_type" @@ -932,12 +923,8 @@ async def test_usb_discovery_migration( context={"source": config_entries.SOURCE_USB}, data=USB_DISCOVERY_INFO, ) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "usb_confirm" + assert mock_usb_serial_by_id.call_count == 2 - - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] is FlowResultType.FORM assert result["step_id"] == "intent_migrate" @@ -1063,12 +1050,8 @@ async def test_usb_discovery_migration_restore_driver_ready_timeout( context={"source": config_entries.SOURCE_USB}, data=USB_DISCOVERY_INFO, ) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "usb_confirm" + assert mock_usb_serial_by_id.call_count == 2 - - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] is FlowResultType.FORM assert result["step_id"] == "intent_migrate" @@ -1366,16 +1349,16 @@ async def test_usb_discovery_with_existing_usb_flow(hass: HomeAssistant) -> None data=first_usb_info, ) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "usb_confirm" + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "installation_type" result2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USB}, data=USB_DISCOVERY_INFO, ) - assert result2["type"] is FlowResultType.FORM - assert result2["step_id"] == "usb_confirm" + assert result2["type"] is FlowResultType.MENU + assert result2["step_id"] == "installation_type" usb_flows_in_progress = hass.config_entries.flow.async_progress_by_handler( DOMAIN, match_context={"source": config_entries.SOURCE_USB} @@ -1409,53 +1392,6 @@ async def test_abort_usb_discovery_addon_required(hass: HomeAssistant) -> None: assert result["reason"] == "addon_required" -@pytest.mark.usefixtures( - "supervisor", - "addon_running", -) -async def test_abort_usb_discovery_confirm_addon_required( - hass: HomeAssistant, - addon_options: dict[str, Any], - mock_usb_serial_by_id: MagicMock, -) -> None: - """Test usb discovery confirm aborted when existing entry not using add-on.""" - addon_options["device"] = "/dev/another_device" - entry = MockConfigEntry( - domain=DOMAIN, - data={ - "url": "ws://localhost:3000", - "usb_path": "/dev/another_device", - "use_addon": True, - }, - title=TITLE, - unique_id="1234", - ) - entry.add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_USB}, - data=USB_DISCOVERY_INFO, - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "usb_confirm" - assert mock_usb_serial_by_id.call_count == 2 - - hass.config_entries.async_update_entry( - entry, - data={ - **entry.data, - "use_addon": False, - }, - ) - - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "addon_required" - - async def test_usb_discovery_requires_supervisor(hass: HomeAssistant) -> None: """Test usb discovery flow is aborted when there is no supervisor.""" result = await hass.config_entries.flow.async_init( @@ -4635,13 +4571,8 @@ async def test_recommended_usb_discovery( context={"source": config_entries.SOURCE_USB}, data=usb_discovery_info, ) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "usb_confirm" - assert result["description_placeholders"] == {"name": discovery_name} + assert mock_usb_serial_by_id.call_count == 1 - - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] is FlowResultType.MENU assert result["step_id"] == "installation_type" assert result["menu_options"] == ["intent_recommended", "intent_custom"] From 2830ed61471a8e1adde182d4abe4e320e83ff05a Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Tue, 27 May 2025 11:04:29 +0300 Subject: [PATCH 010/266] Change description on recommended/custom Z-Wave install step (#145688) Change description on recommended/custom Z-WaveJS step --- homeassistant/components/zwave_js/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json index ee6efcb2fb6..439fc7b1aad 100644 --- a/homeassistant/components/zwave_js/strings.json +++ b/homeassistant/components/zwave_js/strings.json @@ -131,7 +131,7 @@ }, "installation_type": { "title": "Set up Z-Wave", - "description": "Choose the installation type for your Z-Wave integration.", + "description": "In a few steps, we’re going to set up your Home Assistant Connect ZWA-2. Home Assistant can automatically install and configure the recommended Z-Wave setup, or you can customize it.", "menu_options": { "intent_recommended": "Recommended installation", "intent_custom": "Custom installation" From 9e7dc1d11d702eae57f57a5d65bb472c4a062c0c Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 27 May 2025 12:33:02 +0200 Subject: [PATCH 011/266] Use string type for amazon devices OTP code (#145698) --- homeassistant/components/amazon_devices/config_flow.py | 2 +- tests/components/amazon_devices/const.py | 2 +- tests/components/amazon_devices/test_config_flow.py | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/amazon_devices/config_flow.py b/homeassistant/components/amazon_devices/config_flow.py index 5566c16602b..d0c3d067cee 100644 --- a/homeassistant/components/amazon_devices/config_flow.py +++ b/homeassistant/components/amazon_devices/config_flow.py @@ -57,7 +57,7 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN): ): CountrySelector(), vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_CODE): cv.positive_int, + vol.Required(CONF_CODE): cv.string, } ), ) diff --git a/tests/components/amazon_devices/const.py b/tests/components/amazon_devices/const.py index 94b5b7052e6..a2600ba98a6 100644 --- a/tests/components/amazon_devices/const.py +++ b/tests/components/amazon_devices/const.py @@ -1,6 +1,6 @@ """Amazon Devices tests const.""" -TEST_CODE = 123123 +TEST_CODE = "023123" TEST_COUNTRY = "IT" TEST_PASSWORD = "fake_password" TEST_SERIAL_NUMBER = "echo_test_serial_number" diff --git a/tests/components/amazon_devices/test_config_flow.py b/tests/components/amazon_devices/test_config_flow.py index 68ab7f4ffa6..41b65c33bd5 100644 --- a/tests/components/amazon_devices/test_config_flow.py +++ b/tests/components/amazon_devices/test_config_flow.py @@ -56,6 +56,7 @@ async def test_full_flow( }, } assert result["result"].unique_id == TEST_USERNAME + mock_amazon_devices_client.login_mode_interactive.assert_called_once_with("023123") @pytest.mark.parametrize( From b84850df9f6683fdccbdd57a1f3d3920629a2e9b Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 27 May 2025 12:54:57 +0200 Subject: [PATCH 012/266] Fix error stack trace for HomeAssistantError in websocket service call (#145699) * Add test * Fix error stack trace for HomeAssistantError in websocket service call --- homeassistant/components/websocket_api/commands.py | 4 +++- tests/components/websocket_api/test_commands.py | 8 +++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index ddcdd4f1cf8..9c371a8399d 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -300,7 +300,9 @@ async def handle_call_service( translation_placeholders=err.translation_placeholders, ) except HomeAssistantError as err: - connection.logger.exception("Unexpected exception") + connection.logger.error( + "Error during service call to %s.%s: %s", msg["domain"], msg["service"], err + ) connection.send_error( msg["id"], const.ERR_HOME_ASSISTANT_ERROR, diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 4ca2098550b..2c9cc19c84b 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -514,9 +514,12 @@ async def test_call_service_schema_validation_error( @pytest.mark.parametrize("ignore_translations_for_mock_domains", ["test"]) async def test_call_service_error( - hass: HomeAssistant, websocket_client: MockHAClientWebSocket + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + websocket_client: MockHAClientWebSocket, ) -> None: """Test call service command with error.""" + caplog.set_level(logging.ERROR) @callback def ha_error_call(_): @@ -561,6 +564,7 @@ async def test_call_service_error( assert msg["error"]["translation_placeholders"] == {"option": "bla"} assert msg["error"]["translation_key"] == "custom_error" assert msg["error"]["translation_domain"] == "test" + assert "Traceback" not in caplog.text await websocket_client.send_json_auto_id( { @@ -578,6 +582,7 @@ async def test_call_service_error( assert msg["error"]["translation_placeholders"] == {"option": "bla"} assert msg["error"]["translation_key"] == "custom_error" assert msg["error"]["translation_domain"] == "test" + assert "Traceback" not in caplog.text await websocket_client.send_json_auto_id( { @@ -592,6 +597,7 @@ async def test_call_service_error( assert msg["success"] is False assert msg["error"]["code"] == "unknown_error" assert msg["error"]["message"] == "value_error" + assert "Traceback" in caplog.text async def test_subscribe_unsubscribe_events( From 923530972a5e25c78065df1e4cc39afb3bf44a24 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 27 May 2025 17:35:11 +0200 Subject: [PATCH 013/266] Remove static pin code length Matter sensors (#145711) * Remove static Matter sensors * Clean up translation strings --- homeassistant/components/matter/sensor.py | 22 -- homeassistant/components/matter/strings.json | 6 - .../matter/snapshots/test_sensor.ambr | 192 ------------------ 3 files changed, 220 deletions(-) diff --git a/homeassistant/components/matter/sensor.py b/homeassistant/components/matter/sensor.py index 2197f81e134..e1fbf1c5a82 100644 --- a/homeassistant/components/matter/sensor.py +++ b/homeassistant/components/matter/sensor.py @@ -967,28 +967,6 @@ DISCOVERY_SCHEMAS = [ # don't discover this entry if the supported state list is empty secondary_value_is_not=[], ), - MatterDiscoverySchema( - platform=Platform.SENSOR, - entity_description=MatterSensorEntityDescription( - key="MinPINCodeLength", - translation_key="min_pin_code_length", - entity_category=EntityCategory.DIAGNOSTIC, - device_class=None, - ), - entity_class=MatterSensor, - required_attributes=(clusters.DoorLock.Attributes.MinPINCodeLength,), - ), - MatterDiscoverySchema( - platform=Platform.SENSOR, - entity_description=MatterSensorEntityDescription( - key="MaxPINCodeLength", - translation_key="max_pin_code_length", - entity_category=EntityCategory.DIAGNOSTIC, - device_class=None, - ), - entity_class=MatterSensor, - required_attributes=(clusters.DoorLock.Attributes.MaxPINCodeLength,), - ), MatterDiscoverySchema( platform=Platform.SENSOR, entity_description=MatterSensorEntityDescription( diff --git a/homeassistant/components/matter/strings.json b/homeassistant/components/matter/strings.json index a04f1d86880..7cae16c5e9b 100644 --- a/homeassistant/components/matter/strings.json +++ b/homeassistant/components/matter/strings.json @@ -390,12 +390,6 @@ "evse_user_max_charge_current": { "name": "User max charge current" }, - "min_pin_code_length": { - "name": "Min PIN code length" - }, - "max_pin_code_length": { - "name": "Max PIN code length" - }, "window_covering_target_position": { "name": "Target opening position" } diff --git a/tests/components/matter/snapshots/test_sensor.ambr b/tests/components/matter/snapshots/test_sensor.ambr index 3af00db623e..685d3c0022d 100644 --- a/tests/components/matter/snapshots/test_sensor.ambr +++ b/tests/components/matter/snapshots/test_sensor.ambr @@ -1307,198 +1307,6 @@ 'state': '180.0', }) # --- -# name: test_sensors[door_lock][sensor.mock_door_lock_max_pin_code_length-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.mock_door_lock_max_pin_code_length', - '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': 'Max PIN code length', - 'platform': 'matter', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'max_pin_code_length', - 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-MaxPINCodeLength-257-23', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors[door_lock][sensor.mock_door_lock_max_pin_code_length-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Mock Door Lock Max PIN code length', - }), - 'context': , - 'entity_id': 'sensor.mock_door_lock_max_pin_code_length', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '8', - }) -# --- -# name: test_sensors[door_lock][sensor.mock_door_lock_min_pin_code_length-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.mock_door_lock_min_pin_code_length', - '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': 'Min PIN code length', - 'platform': 'matter', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'min_pin_code_length', - 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-MinPINCodeLength-257-24', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors[door_lock][sensor.mock_door_lock_min_pin_code_length-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Mock Door Lock Min PIN code length', - }), - 'context': , - 'entity_id': 'sensor.mock_door_lock_min_pin_code_length', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '6', - }) -# --- -# name: test_sensors[door_lock_with_unbolt][sensor.mock_door_lock_max_pin_code_length-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.mock_door_lock_max_pin_code_length', - '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': 'Max PIN code length', - 'platform': 'matter', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'max_pin_code_length', - 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-MaxPINCodeLength-257-23', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors[door_lock_with_unbolt][sensor.mock_door_lock_max_pin_code_length-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Mock Door Lock Max PIN code length', - }), - 'context': , - 'entity_id': 'sensor.mock_door_lock_max_pin_code_length', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '8', - }) -# --- -# name: test_sensors[door_lock_with_unbolt][sensor.mock_door_lock_min_pin_code_length-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.mock_door_lock_min_pin_code_length', - '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': 'Min PIN code length', - 'platform': 'matter', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'min_pin_code_length', - 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-MinPINCodeLength-257-24', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors[door_lock_with_unbolt][sensor.mock_door_lock_min_pin_code_length-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Mock Door Lock Min PIN code length', - }), - 'context': , - 'entity_id': 'sensor.mock_door_lock_min_pin_code_length', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '6', - }) -# --- # name: test_sensors[eve_contact_sensor][sensor.eve_door_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From 389becc4f6397886c7a552243bbceac3205e36c0 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 27 May 2025 17:46:21 +0200 Subject: [PATCH 014/266] Disable advanced window cover position Matter sensor by default (#145713) * Disable advanced window cover position Matter sensor by default * Enanble disabled sensors in snapshot test --- homeassistant/components/matter/sensor.py | 1 + .../matter/snapshots/test_sensor.ambr | 306 ++++++++++++++++++ tests/components/matter/test_sensor.py | 2 +- 3 files changed, 308 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/matter/sensor.py b/homeassistant/components/matter/sensor.py index e1fbf1c5a82..70e4cb238f5 100644 --- a/homeassistant/components/matter/sensor.py +++ b/homeassistant/components/matter/sensor.py @@ -972,6 +972,7 @@ DISCOVERY_SCHEMAS = [ entity_description=MatterSensorEntityDescription( key="TargetPositionLiftPercent100ths", entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, translation_key="window_covering_target_position", measurement_to_ha=lambda x: round((10000 - x) / 100), native_unit_of_measurement=PERCENTAGE, diff --git a/tests/components/matter/snapshots/test_sensor.ambr b/tests/components/matter/snapshots/test_sensor.ambr index 685d3c0022d..3a5a937b4a4 100644 --- a/tests/components/matter/snapshots/test_sensor.ambr +++ b/tests/components/matter/snapshots/test_sensor.ambr @@ -2425,6 +2425,159 @@ 'state': '0.0', }) # --- +# name: test_sensors[generic_switch][sensor.mock_generic_switch_current_switch_position-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.mock_generic_switch_current_switch_position', + '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': 'Current switch position', + 'platform': 'matter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'switch_current_position', + 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-SwitchCurrentPosition-59-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[generic_switch][sensor.mock_generic_switch_current_switch_position-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Generic Switch Current switch position', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.mock_generic_switch_current_switch_position', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[generic_switch_multi][sensor.mock_generic_switch_current_switch_position_1-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.mock_generic_switch_current_switch_position_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': 'Current switch position (1)', + 'platform': 'matter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'switch_current_position', + 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-SwitchCurrentPosition-59-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[generic_switch_multi][sensor.mock_generic_switch_current_switch_position_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Generic Switch Current switch position (1)', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.mock_generic_switch_current_switch_position_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[generic_switch_multi][sensor.mock_generic_switch_fancy_button-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.mock_generic_switch_fancy_button', + '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': 'Fancy Button', + 'platform': 'matter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'switch_current_position', + 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-2-SwitchCurrentPosition-59-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[generic_switch_multi][sensor.mock_generic_switch_fancy_button-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Generic Switch Fancy Button', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.mock_generic_switch_fancy_button', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- # name: test_sensors[humidity_sensor][sensor.mock_humidity_sensor_humidity-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -2715,6 +2868,159 @@ 'state': 'stopped', }) # --- +# name: test_sensors[multi_endpoint_light][sensor.inovelli_config-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.inovelli_config', + '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': 'Config', + 'platform': 'matter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'switch_current_position', + 'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-5-SwitchCurrentPosition-59-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[multi_endpoint_light][sensor.inovelli_config-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Inovelli Config', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.inovelli_config', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[multi_endpoint_light][sensor.inovelli_down-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.inovelli_down', + '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': 'Down', + 'platform': 'matter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'switch_current_position', + 'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-4-SwitchCurrentPosition-59-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[multi_endpoint_light][sensor.inovelli_down-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Inovelli Down', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.inovelli_down', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[multi_endpoint_light][sensor.inovelli_up-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.inovelli_up', + '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': 'Up', + 'platform': 'matter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'switch_current_position', + 'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-3-SwitchCurrentPosition-59-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[multi_endpoint_light][sensor.inovelli_up-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Inovelli Up', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.inovelli_up', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- # name: test_sensors[oven][sensor.mock_oven_current_phase-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/matter/test_sensor.py b/tests/components/matter/test_sensor.py index 19697efab71..e15e3f9f53e 100644 --- a/tests/components/matter/test_sensor.py +++ b/tests/components/matter/test_sensor.py @@ -17,7 +17,7 @@ from .common import ( ) -@pytest.mark.usefixtures("matter_devices") +@pytest.mark.usefixtures("entity_registry_enabled_by_default", "matter_devices") async def test_sensors( hass: HomeAssistant, entity_registry: er.EntityRegistry, From 8880ab64987920a77cad1bdf1c6008c4de74f2d4 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Tue, 27 May 2025 18:45:49 +0200 Subject: [PATCH 015/266] Catch PermissionDenied(Route API disabled) in google_travel_time (#145722) Catch PermissionDenied(Route API disabled) --- .../google_travel_time/config_flow.py | 9 ++++- .../components/google_travel_time/helpers.py | 39 +++++++++++++++++++ .../components/google_travel_time/sensor.py | 13 ++++++- .../google_travel_time/strings.json | 7 ++++ .../google_travel_time/test_config_flow.py | 13 ++++++- .../google_travel_time/test_sensor.py | 26 ++++++++++++- 6 files changed, 102 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/google_travel_time/config_flow.py b/homeassistant/components/google_travel_time/config_flow.py index 24ea29aef03..9e07fdefe9d 100644 --- a/homeassistant/components/google_travel_time/config_flow.py +++ b/homeassistant/components/google_travel_time/config_flow.py @@ -50,7 +50,12 @@ from .const import ( UNITS_IMPERIAL, UNITS_METRIC, ) -from .helpers import InvalidApiKeyException, UnknownException, validate_config_entry +from .helpers import ( + InvalidApiKeyException, + PermissionDeniedException, + UnknownException, + validate_config_entry, +) RECONFIGURE_SCHEMA = vol.Schema( { @@ -188,6 +193,8 @@ async def validate_input( user_input[CONF_ORIGIN], user_input[CONF_DESTINATION], ) + except PermissionDeniedException: + return {"base": "permission_denied"} except InvalidApiKeyException: return {"base": "invalid_auth"} except TimeoutError: diff --git a/homeassistant/components/google_travel_time/helpers.py b/homeassistant/components/google_travel_time/helpers.py index 2e36abd62b1..70f9300c92f 100644 --- a/homeassistant/components/google_travel_time/helpers.py +++ b/homeassistant/components/google_travel_time/helpers.py @@ -7,6 +7,7 @@ from google.api_core.exceptions import ( Forbidden, GatewayTimeout, GoogleAPIError, + PermissionDenied, Unauthorized, ) from google.maps.routing_v2 import ( @@ -19,10 +20,18 @@ from google.maps.routing_v2 import ( from google.type import latlng_pb2 import voluptuous as vol +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.issue_registry import ( + IssueSeverity, + async_create_issue, + async_delete_issue, +) from homeassistant.helpers.location import find_coordinates +from .const import DOMAIN + _LOGGER = logging.getLogger(__name__) @@ -67,6 +76,9 @@ async def validate_config_entry( await client.compute_routes( request, metadata=[("x-goog-fieldmask", field_mask)] ) + except PermissionDenied as permission_error: + _LOGGER.error("Permission denied: %s", permission_error.message) + raise PermissionDeniedException from permission_error except (Unauthorized, Forbidden) as unauthorized_error: _LOGGER.error("Request denied: %s", unauthorized_error.message) raise InvalidApiKeyException from unauthorized_error @@ -84,3 +96,30 @@ class InvalidApiKeyException(Exception): class UnknownException(Exception): """Unknown API Error.""" + + +class PermissionDeniedException(Exception): + """Permission Denied Error.""" + + +def create_routes_api_disabled_issue(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Create an issue for the Routes API being disabled.""" + async_create_issue( + hass, + DOMAIN, + f"routes_api_disabled_{entry.entry_id}", + learn_more_url="https://www.home-assistant.io/integrations/google_travel_time#setup", + is_fixable=False, + severity=IssueSeverity.ERROR, + translation_key="routes_api_disabled", + translation_placeholders={ + "entry_title": entry.title, + "enable_api_url": "https://cloud.google.com/endpoints/docs/openapi/enable-api", + "api_key_restrictions_url": "https://cloud.google.com/docs/authentication/api-keys#adding-api-restrictions", + }, + ) + + +def delete_routes_api_disabled_issue(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Delete the issue for the Routes API being disabled.""" + async_delete_issue(hass, DOMAIN, f"routes_api_disabled_{entry.entry_id}") diff --git a/homeassistant/components/google_travel_time/sensor.py b/homeassistant/components/google_travel_time/sensor.py index 7448fc1cb09..6323813b759 100644 --- a/homeassistant/components/google_travel_time/sensor.py +++ b/homeassistant/components/google_travel_time/sensor.py @@ -7,7 +7,7 @@ import logging from typing import TYPE_CHECKING, Any from google.api_core.client_options import ClientOptions -from google.api_core.exceptions import GoogleAPIError +from google.api_core.exceptions import GoogleAPIError, PermissionDenied from google.maps.routing_v2 import ( ComputeRoutesRequest, Route, @@ -58,7 +58,11 @@ from .const import ( TRAVEL_MODES_TO_GOOGLE_SDK_ENUM, UNITS_TO_GOOGLE_SDK_ENUM, ) -from .helpers import convert_to_waypoint +from .helpers import ( + convert_to_waypoint, + create_routes_api_disabled_issue, + delete_routes_api_disabled_issue, +) _LOGGER = logging.getLogger(__name__) @@ -273,6 +277,11 @@ class GoogleTravelTimeSensor(SensorEntity): ) if response is not None and len(response.routes) > 0: self._route = response.routes[0] + delete_routes_api_disabled_issue(self.hass, self._config_entry) + except PermissionDenied: + _LOGGER.error("Routes API is disabled for this API key") + create_routes_api_disabled_issue(self.hass, self._config_entry) + self._route = None except GoogleAPIError as ex: _LOGGER.error("Error getting travel time: %s", ex) self._route = None diff --git a/homeassistant/components/google_travel_time/strings.json b/homeassistant/components/google_travel_time/strings.json index 87bc09eb456..f46d33fda09 100644 --- a/homeassistant/components/google_travel_time/strings.json +++ b/homeassistant/components/google_travel_time/strings.json @@ -21,6 +21,7 @@ } }, "error": { + "permission_denied": "The Routes API is not enabled for this API key. Please see the setup instructions for detailed information.", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "timeout_connect": "[%key:common::config_flow::error::timeout_connect%]" @@ -100,5 +101,11 @@ "fewer_transfers": "Fewer transfers" } } + }, + "issues": { + "routes_api_disabled": { + "title": "The Routes API must be enabled", + "description": "Your Google Travel Time integration `{entry_title}` uses an API key which does not have the Routes API enabled.\n\n Please follow the instructions to [enable the API for your project]({enable_api_url}) and make sure your [API key restrictions]({api_key_restrictions_url}) allow access to the Routes API.\n\n After enabling the API this issue will be resolved automatically." + } } } diff --git a/tests/components/google_travel_time/test_config_flow.py b/tests/components/google_travel_time/test_config_flow.py index 8cdb3c270d0..562ca152ce8 100644 --- a/tests/components/google_travel_time/test_config_flow.py +++ b/tests/components/google_travel_time/test_config_flow.py @@ -2,7 +2,12 @@ from unittest.mock import AsyncMock, patch -from google.api_core.exceptions import GatewayTimeout, GoogleAPIError, Unauthorized +from google.api_core.exceptions import ( + GatewayTimeout, + GoogleAPIError, + PermissionDenied, + Unauthorized, +) import pytest from homeassistant.components.google_travel_time.const import ( @@ -98,6 +103,12 @@ async def test_minimum_fields(hass: HomeAssistant) -> None: (GoogleAPIError("test"), "cannot_connect"), (GatewayTimeout("Timeout error."), "timeout_connect"), (Unauthorized("Invalid API key."), "invalid_auth"), + ( + PermissionDenied( + "Requests to this API routes.googleapis.com method google.maps.routing.v2.Routes.ComputeRoutes are blocked." + ), + "permission_denied", + ), ], ) async def test_errors( diff --git a/tests/components/google_travel_time/test_sensor.py b/tests/components/google_travel_time/test_sensor.py index 58843d8275c..0ab5e38a644 100644 --- a/tests/components/google_travel_time/test_sensor.py +++ b/tests/components/google_travel_time/test_sensor.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock from freezegun.api import FrozenDateTimeFactory -from google.api_core.exceptions import GoogleAPIError +from google.api_core.exceptions import GoogleAPIError, PermissionDenied from google.maps.routing_v2 import Units import pytest @@ -20,6 +20,7 @@ from homeassistant.components.google_travel_time.const import ( from homeassistant.components.google_travel_time.sensor import SCAN_INTERVAL from homeassistant.const import CONF_MODE, STATE_UNKNOWN from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir from homeassistant.util.unit_system import ( METRIC_SYSTEM, US_CUSTOMARY_SYSTEM, @@ -170,3 +171,26 @@ async def test_sensor_exception( await hass.async_block_till_done() assert hass.states.get("sensor.google_travel_time").state == STATE_UNKNOWN assert "Error getting travel time" in caplog.text + + +@pytest.mark.parametrize( + ("data", "options"), + [(MOCK_CONFIG, DEFAULT_OPTIONS)], +) +async def test_sensor_routes_api_disabled( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + routes_mock: AsyncMock, + mock_config: MockConfigEntry, + freezer: FrozenDateTimeFactory, + issue_registry: ir.IssueRegistry, +) -> None: + """Test that exception gets caught and issue created.""" + routes_mock.compute_routes.side_effect = PermissionDenied("Errormessage") + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert hass.states.get("sensor.google_travel_time").state == STATE_UNKNOWN + assert "Routes API is disabled for this API key" in caplog.text + + assert len(issue_registry.issues) == 1 From 41a140d16c6b78d1c204f6adeb1277315e1b4426 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Tue, 27 May 2025 17:44:48 +0200 Subject: [PATCH 016/266] Debug log the update response in google_travel_time (#145725) Debug log the update response --- homeassistant/components/google_travel_time/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/google_travel_time/sensor.py b/homeassistant/components/google_travel_time/sensor.py index 6323813b759..1a9b361bd33 100644 --- a/homeassistant/components/google_travel_time/sensor.py +++ b/homeassistant/components/google_travel_time/sensor.py @@ -275,6 +275,7 @@ class GoogleTravelTimeSensor(SensorEntity): response = await self._client.compute_routes( request, metadata=[("x-goog-fieldmask", FIELD_MASK)] ) + _LOGGER.debug("Received response: %s", response) if response is not None and len(response.routes) > 0: self._route = response.routes[0] delete_routes_api_disabled_issue(self.hass, self._config_entry) From 6e6aae2ea3594743d451e63679325cdc89f2669a Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 27 May 2025 19:29:04 +0200 Subject: [PATCH 017/266] Fix unbound local variable in Acmeda config flow (#145729) --- homeassistant/components/acmeda/config_flow.py | 3 ++- tests/components/acmeda/test_config_flow.py | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/acmeda/config_flow.py b/homeassistant/components/acmeda/config_flow.py index 5024507a7d3..785906ebf2a 100644 --- a/homeassistant/components/acmeda/config_flow.py +++ b/homeassistant/components/acmeda/config_flow.py @@ -40,9 +40,10 @@ class AcmedaFlowHandler(ConfigFlow, domain=DOMAIN): entry.unique_id for entry in self._async_current_entries() } + hubs: list[aiopulse.Hub] = [] with suppress(TimeoutError): async with timeout(5): - hubs: list[aiopulse.Hub] = [ + hubs = [ hub async for hub in aiopulse.Hub.discover() if hub.id not in already_configured diff --git a/tests/components/acmeda/test_config_flow.py b/tests/components/acmeda/test_config_flow.py index 7b92c1aac3b..6589013d432 100644 --- a/tests/components/acmeda/test_config_flow.py +++ b/tests/components/acmeda/test_config_flow.py @@ -49,6 +49,21 @@ async def test_show_form_no_hubs(hass: HomeAssistant, mock_hub_discover) -> None assert len(mock_hub_discover.mock_calls) == 1 +async def test_timeout_fetching_hub(hass: HomeAssistant, mock_hub_discover) -> None: + """Test that flow aborts if no hubs are discovered.""" + mock_hub_discover.side_effect = TimeoutError + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + # Check we performed the discovery + assert len(mock_hub_discover.mock_calls) == 1 + + @pytest.mark.usefixtures("mock_hub_run") async def test_show_form_one_hub(hass: HomeAssistant, mock_hub_discover) -> None: """Test that a config is created when one hub discovered.""" From 6adb27d17326147ac56e1a54d9a00651655f41fa Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Tue, 27 May 2025 21:27:52 +0200 Subject: [PATCH 018/266] Tado update mobile devices interval (#145738) Update the mobile devices interval to five minutes --- homeassistant/components/tado/coordinator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tado/coordinator.py b/homeassistant/components/tado/coordinator.py index 5f3aa1de1e4..09c6ec40208 100644 --- a/homeassistant/components/tado/coordinator.py +++ b/homeassistant/components/tado/coordinator.py @@ -31,7 +31,7 @@ _LOGGER = logging.getLogger(__name__) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=4) SCAN_INTERVAL = timedelta(minutes=5) -SCAN_MOBILE_DEVICE_INTERVAL = timedelta(seconds=30) +SCAN_MOBILE_DEVICE_INTERVAL = timedelta(minutes=5) class TadoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]): From 3160fe9abcf1961408894cb0bb3a50f077727c97 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 27 May 2025 22:12:07 +0200 Subject: [PATCH 019/266] Update frontend to 20250527.0 (#145741) --- 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 fe445ae6b28..32d243b3431 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==20250526.0"] + "requirements": ["home-assistant-frontend==20250527.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index ae59ce94200..378e9fdce83 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ habluetooth==3.48.2 hass-nabucasa==0.101.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250526.0 +home-assistant-frontend==20250527.0 home-assistant-intents==2025.5.7 httpx==0.28.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 414153e193e..a54a12ee9c4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1164,7 +1164,7 @@ hole==0.8.0 holidays==0.73 # homeassistant.components.frontend -home-assistant-frontend==20250526.0 +home-assistant-frontend==20250527.0 # homeassistant.components.conversation home-assistant-intents==2025.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f858d8e4315..f381de177dd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -994,7 +994,7 @@ hole==0.8.0 holidays==0.73 # homeassistant.components.frontend -home-assistant-frontend==20250526.0 +home-assistant-frontend==20250527.0 # homeassistant.components.conversation home-assistant-intents==2025.5.7 From 10adb57b837906d529b826e2740a741bd4d52db2 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 27 May 2025 22:16:13 +0200 Subject: [PATCH 020/266] Bump version to 2025.6.0b1 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index a36aa88ca84..6b3cbb4f27c 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 = 6 -PATCH_VERSION: Final = "0b0" +PATCH_VERSION: Final = "0b1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index 7df9230b7ee..7e1e98bdb24 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.6.0b0" +version = "2025.6.0b1" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From adddf330fd835fcef52e90ade508feb79b646963 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 28 May 2025 10:16:40 +0200 Subject: [PATCH 021/266] Ensure mqtt sensor unit of measurement validation for state class `measurement_angle` (#145648) --- homeassistant/components/mqtt/config_flow.py | 24 +++++++++++++++--- homeassistant/components/mqtt/sensor.py | 12 +++++++++ homeassistant/components/mqtt/strings.json | 1 + tests/components/mqtt/test_config_flow.py | 10 +++++++- tests/components/mqtt/test_sensor.py | 26 ++++++++++++++++++++ 5 files changed, 68 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index bb884d6392f..b41e549093d 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -39,6 +39,7 @@ from homeassistant.components.light import ( from homeassistant.components.sensor import ( CONF_STATE_CLASS, DEVICE_CLASS_UNITS, + STATE_CLASS_UNITS, SensorDeviceClass, SensorStateClass, ) @@ -640,6 +641,13 @@ def validate_sensor_platform_config( ): errors[CONF_UNIT_OF_MEASUREMENT] = "invalid_uom" + if ( + (state_class := config.get(CONF_STATE_CLASS)) is not None + and state_class in STATE_CLASS_UNITS + and config.get(CONF_UNIT_OF_MEASUREMENT) not in STATE_CLASS_UNITS[state_class] + ): + errors[CONF_UNIT_OF_MEASUREMENT] = "invalid_uom_for_state_class" + return errors @@ -676,11 +684,19 @@ class PlatformField: @callback def unit_of_measurement_selector(user_data: dict[str, Any | None]) -> Selector: """Return a context based unit of measurement selector.""" + + if (state_class := user_data.get(CONF_STATE_CLASS)) in STATE_CLASS_UNITS: + return SelectSelector( + SelectSelectorConfig( + options=[str(uom) for uom in STATE_CLASS_UNITS[state_class]], + sort=True, + custom_value=True, + ) + ) + if ( - user_data is None - or (device_class := user_data.get(CONF_DEVICE_CLASS)) is None - or device_class not in DEVICE_CLASS_UNITS - ): + device_class := user_data.get(CONF_DEVICE_CLASS) + ) is None or device_class not in DEVICE_CLASS_UNITS: return TEXT_SELECTOR return SelectSelector( SelectSelectorConfig( diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index b27ef68368a..46d475fcee8 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -14,6 +14,7 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_UNITS, DEVICE_CLASSES_SCHEMA, ENTITY_ID_FORMAT, + STATE_CLASS_UNITS, STATE_CLASSES_SCHEMA, RestoreSensor, SensorDeviceClass, @@ -117,6 +118,17 @@ def validate_sensor_state_and_device_class_config(config: ConfigType) -> ConfigT f"got `{CONF_DEVICE_CLASS}` '{device_class}'" ) + if ( + (state_class := config.get(CONF_STATE_CLASS)) is not None + and state_class in STATE_CLASS_UNITS + and (unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT)) + not in STATE_CLASS_UNITS[state_class] + ): + raise vol.Invalid( + f"The unit of measurement '{unit_of_measurement}' is not valid " + f"together with state class '{state_class}'" + ) + if (device_class := config.get(CONF_DEVICE_CLASS)) is None or ( unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT) ) is None: diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 8fc97362857..9bc6df1b633 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -644,6 +644,7 @@ "invalid_template": "Invalid template", "invalid_supported_color_modes": "Invalid supported color modes selection", "invalid_uom": "The unit of measurement \"{unit_of_measurement}\" is not supported by the selected device class, please either remove the device class, select a device class which supports \"{unit_of_measurement}\", or pick a supported unit of measurement from the list", + "invalid_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_kelvin": "Max Kelvin value should be greater than min Kelvin value", diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index a43617badb0..e30aa5d50d6 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -3038,7 +3038,15 @@ async def test_migrate_of_incompatible_config_entry( { "state_class": "measurement", }, - (), + ( + ( + { + "state_class": "measurement_angle", + "unit_of_measurement": "deg", + }, + {"unit_of_measurement": "invalid_uom_for_state_class"}, + ), + ), { "state_topic": "test-topic", }, diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 0bafacfed26..ea1b7e186e2 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -995,6 +995,32 @@ async def test_invalid_state_class( assert "expected SensorStateClass or one of" in caplog.text +@pytest.mark.parametrize( + "hass_config", + [ + { + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "state_class": "measurement_angle", + "unit_of_measurement": "deg", + } + } + } + ], +) +async def test_invalid_state_class_with_unit_of_measurement( + mqtt_mock_entry: MqttMockHAClientGenerator, caplog: pytest.LogCaptureFixture +) -> None: + """Test state_class option with invalid unit of measurement.""" + assert await mqtt_mock_entry() + assert ( + "The unit of measurement 'deg' is not valid together with state class 'measurement_angle'" + in caplog.text + ) + + @pytest.mark.parametrize( ("hass_config", "error_logged"), [ From f86bf69ebcd5a13d142b0371469eb857c92b3a60 Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Wed, 28 May 2025 08:13:28 +0200 Subject: [PATCH 022/266] Update otp description for amazon_devices (#145701) * Update otp description from amazon_devices * separate * Update strings.json --- homeassistant/components/amazon_devices/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/amazon_devices/strings.json b/homeassistant/components/amazon_devices/strings.json index 8db249b44ed..47e6234cd9c 100644 --- a/homeassistant/components/amazon_devices/strings.json +++ b/homeassistant/components/amazon_devices/strings.json @@ -5,7 +5,7 @@ "data_description_country": "The country of your Amazon account.", "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 sent to your email address." + "data_description_code": "The one-time password to log in to your account. Currently, only tokens from OTP applications are supported." }, "config": { "flow_title": "{username}", From 0e7a1bb76cf1890c9bcfb6ad36039ec5e7a47345 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 27 May 2025 23:00:52 +0200 Subject: [PATCH 023/266] Make async_remove_stale_devices_links_keep_entity_device move entities (#145719) Co-authored-by: Jan Bouwhuis Co-authored-by: Joost Lekkerkerker --- homeassistant/helpers/device.py | 20 ++++++----- tests/helpers/test_device.py | 60 ++++++++++++++++++++------------- 2 files changed, 49 insertions(+), 31 deletions(-) diff --git a/homeassistant/helpers/device.py b/homeassistant/helpers/device.py index 16212422236..a7d888900b1 100644 --- a/homeassistant/helpers/device.py +++ b/homeassistant/helpers/device.py @@ -64,10 +64,10 @@ def async_remove_stale_devices_links_keep_entity_device( entry_id: str, source_entity_id_or_uuid: str, ) -> None: - """Remove the link between stale devices and a configuration entry. + """Remove entry_id from all devices except that of source_entity_id_or_uuid. - Only the device passed in the source_entity_id_or_uuid parameter - linked to the configuration entry will be maintained. + Also moves all entities linked to the entry_id to the device of + source_entity_id_or_uuid. """ async_remove_stale_devices_links_keep_current_device( @@ -83,13 +83,17 @@ def async_remove_stale_devices_links_keep_current_device( entry_id: str, current_device_id: str | None, ) -> None: - """Remove the link between stale devices and a configuration entry. - - Only the device passed in the current_device_id parameter linked to - the configuration entry will be maintained. - """ + """Remove entry_id from all devices except current_device_id.""" dev_reg = dr.async_get(hass) + ent_reg = er.async_get(hass) + + # Make sure all entities are linked to the correct device + for entity in ent_reg.entities.get_entries_for_config_entry_id(entry_id): + if entity.device_id == current_device_id: + continue + ent_reg.async_update_entity(entity.entity_id, device_id=current_device_id) + # Removes all devices from the config entry that are not the same as the current device for device in dev_reg.devices.get_devices_for_config_entry_id(entry_id): if device.id == current_device_id: diff --git a/tests/helpers/test_device.py b/tests/helpers/test_device.py index 852d418da23..266435ef05d 100644 --- a/tests/helpers/test_device.py +++ b/tests/helpers/test_device.py @@ -118,61 +118,75 @@ async def test_remove_stale_device_links_keep_entity_device( entity_registry: er.EntityRegistry, ) -> None: """Test cleaning works for entity.""" - config_entry = MockConfigEntry(domain="hue") - config_entry.add_to_hass(hass) + helper_config_entry = MockConfigEntry(domain="helper_integration") + helper_config_entry.add_to_hass(hass) + host_config_entry = MockConfigEntry(domain="host_integration") + host_config_entry.add_to_hass(hass) current_device = device_registry.async_get_or_create( identifiers={("test", "current_device")}, connections={("mac", "30:31:32:33:34:00")}, - config_entry_id=config_entry.entry_id, + config_entry_id=helper_config_entry.entry_id, ) - assert current_device is not None - device_registry.async_get_or_create( + stale_device_1 = device_registry.async_get_or_create( identifiers={("test", "stale_device_1")}, connections={("mac", "30:31:32:33:34:01")}, - config_entry_id=config_entry.entry_id, + config_entry_id=helper_config_entry.entry_id, ) device_registry.async_get_or_create( identifiers={("test", "stale_device_2")}, connections={("mac", "30:31:32:33:34:02")}, - config_entry_id=config_entry.entry_id, + config_entry_id=helper_config_entry.entry_id, ) - # Source entity registry + # Source entity source_entity = entity_registry.async_get_or_create( "sensor", - "test", + "host_integration", "source", - config_entry=config_entry, + config_entry=host_config_entry, device_id=current_device.id, ) - await hass.async_block_till_done() - assert entity_registry.async_get("sensor.test_source") is not None + assert entity_registry.async_get(source_entity.entity_id) is not None - devices_config_entry = device_registry.devices.get_devices_for_config_entry_id( - config_entry.entry_id + # Helper entity connected to a stale device + helper_entity = entity_registry.async_get_or_create( + "sensor", + "helper_integration", + "helper", + config_entry=helper_config_entry, + device_id=stale_device_1.id, + ) + assert entity_registry.async_get(helper_entity.entity_id) is not None + + devices_helper_entry = device_registry.devices.get_devices_for_config_entry_id( + helper_config_entry.entry_id ) # 3 devices linked to the config entry are expected (1 current device + 2 stales) - assert len(devices_config_entry) == 3 + assert len(devices_helper_entry) == 3 - # Manual cleanup should unlink stales devices from the config entry + # Manual cleanup should unlink stale devices from the config entry async_remove_stale_devices_links_keep_entity_device( hass, - entry_id=config_entry.entry_id, + entry_id=helper_config_entry.entry_id, source_entity_id_or_uuid=source_entity.entity_id, ) - devices_config_entry = device_registry.devices.get_devices_for_config_entry_id( - config_entry.entry_id + await hass.async_block_till_done() + + devices_helper_entry = device_registry.devices.get_devices_for_config_entry_id( + helper_config_entry.entry_id ) - # After cleanup, only one device is expected to be linked to the config entry - assert len(devices_config_entry) == 1 - - assert current_device in devices_config_entry + # After cleanup, only one device is expected to be linked to the config entry, and + # the entities should exist and be linked to the current device + assert len(devices_helper_entry) == 1 + assert current_device in devices_helper_entry + assert entity_registry.async_get(source_entity.entity_id) is not None + assert entity_registry.async_get(helper_entity.entity_id) is not None async def test_remove_stale_devices_links_keep_current_device( From cd133cbbe38f4042e99e9c49e4c4d8853b382a67 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 28 May 2025 20:51:27 +0200 Subject: [PATCH 024/266] Add level of collections in Immich media source tree (#145734) * add layer for collections in media source tree * re-arange tests, add test for collection layer * fix --- .../components/immich/media_source.py | 46 ++++-- tests/components/immich/test_media_source.py | 149 ++++++++++-------- 2 files changed, 116 insertions(+), 79 deletions(-) diff --git a/homeassistant/components/immich/media_source.py b/homeassistant/components/immich/media_source.py index 201076f1295..0d0616875c6 100644 --- a/homeassistant/components/immich/media_source.py +++ b/homeassistant/components/immich/media_source.py @@ -43,11 +43,12 @@ class ImmichMediaSourceIdentifier: def __init__(self, identifier: str) -> None: """Split identifier into parts.""" parts = identifier.split("/") - # coonfig_entry.unique_id/album_id/asset_it/filename + # config_entry.unique_id/collection/collection_id/asset_id/file_name self.unique_id = parts[0] - self.album_id = parts[1] if len(parts) > 1 else None - self.asset_id = parts[2] if len(parts) > 2 else None - self.file_name = parts[3] if len(parts) > 2 else None + self.collection = parts[1] if len(parts) > 1 else None + self.collection_id = parts[2] if len(parts) > 2 else None + self.asset_id = parts[3] if len(parts) > 3 else None + self.file_name = parts[4] if len(parts) > 3 else None class ImmichMediaSource(MediaSource): @@ -87,6 +88,7 @@ class ImmichMediaSource(MediaSource): ) -> list[BrowseMediaSource]: """Handle browsing different immich instances.""" if not item.identifier: + LOGGER.debug("Render all Immich instances") return [ BrowseMediaSource( domain=DOMAIN, @@ -108,8 +110,22 @@ class ImmichMediaSource(MediaSource): assert entry immich_api = entry.runtime_data.api - if identifier.album_id is None: - # Get Albums + if identifier.collection is None: + LOGGER.debug("Render all collections for %s", entry.title) + return [ + BrowseMediaSource( + domain=DOMAIN, + identifier=f"{identifier.unique_id}/albums", + media_class=MediaClass.DIRECTORY, + media_content_type=MediaClass.IMAGE, + title="albums", + can_play=False, + can_expand=True, + ) + ] + + if identifier.collection_id is None: + LOGGER.debug("Render all albums for %s", entry.title) try: albums = await immich_api.albums.async_get_all_albums() except ImmichError: @@ -118,7 +134,7 @@ class ImmichMediaSource(MediaSource): return [ BrowseMediaSource( domain=DOMAIN, - identifier=f"{item.identifier}/{album.album_id}", + identifier=f"{identifier.unique_id}/albums/{album.album_id}", media_class=MediaClass.DIRECTORY, media_content_type=MediaClass.IMAGE, title=album.name, @@ -129,10 +145,14 @@ class ImmichMediaSource(MediaSource): for album in albums ] - # Request items of album + LOGGER.debug( + "Render all assets of album %s for %s", + identifier.collection_id, + entry.title, + ) try: album_info = await immich_api.albums.async_get_album_info( - identifier.album_id + identifier.collection_id ) except ImmichError: return [] @@ -141,8 +161,8 @@ class ImmichMediaSource(MediaSource): BrowseMediaSource( domain=DOMAIN, identifier=( - f"{identifier.unique_id}/" - f"{identifier.album_id}/" + f"{identifier.unique_id}/albums/" + f"{identifier.collection_id}/" f"{asset.asset_id}/" f"{asset.file_name}" ), @@ -161,8 +181,8 @@ class ImmichMediaSource(MediaSource): BrowseMediaSource( domain=DOMAIN, identifier=( - f"{identifier.unique_id}/" - f"{identifier.album_id}/" + f"{identifier.unique_id}/albums/" + f"{identifier.collection_id}/" f"{asset.asset_id}/" f"{asset.file_name}" ), diff --git a/tests/components/immich/test_media_source.py b/tests/components/immich/test_media_source.py index c8da8d94eeb..0f448fbf23d 100644 --- a/tests/components/immich/test_media_source.py +++ b/tests/components/immich/test_media_source.py @@ -44,8 +44,8 @@ async def test_get_media_source(hass: HomeAssistant) -> None: ("identifier", "exception_msg"), [ ("unique_id", "No file name"), - ("unique_id/album_id", "No file name"), - ("unique_id/album_id/asset_id/filename", "No file extension"), + ("unique_id/albums/album_id", "No file name"), + ("unique_id/albums/album_id/asset_id/filename", "No file extension"), ], ) async def test_resolve_media_bad_identifier( @@ -64,12 +64,12 @@ async def test_resolve_media_bad_identifier( ("identifier", "url", "mime_type"), [ ( - "unique_id/album_id/asset_id/filename.jpg", + "unique_id/albums/album_id/asset_id/filename.jpg", "/immich/unique_id/asset_id/filename.jpg/fullsize", "image/jpeg", ), ( - "unique_id/album_id/asset_id/filename.png", + "unique_id/albums/album_id/asset_id/filename.png", "/immich/unique_id/asset_id/filename.png/fullsize", "image/png", ), @@ -95,13 +95,82 @@ async def test_browse_media_unconfigured(hass: HomeAssistant) -> None: source = await async_get_media_source(hass) item = MediaSourceItem( - hass, DOMAIN, "unique_id/album_id/asset_id/filename.png", None + hass, DOMAIN, "unique_id/albums/album_id/asset_id/filename.png", None ) with pytest.raises(BrowseError, match="Immich is not configured"): await source.async_browse_media(item) -async def test_browse_media_album_error( +async def test_browse_media_get_root( + hass: HomeAssistant, + mock_immich: Mock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test browse_media returning root media sources.""" + assert await async_setup_component(hass, "media_source", {}) + + with patch("homeassistant.components.immich.PLATFORMS", []): + await setup_integration(hass, mock_config_entry) + + source = await async_get_media_source(hass) + + # get root + item = MediaSourceItem(hass, DOMAIN, "", None) + result = await source.async_browse_media(item) + + assert result + assert len(result.children) == 1 + media_file = result.children[0] + assert isinstance(media_file, BrowseMedia) + assert media_file.title == "Someone" + assert media_file.media_content_id == ( + "media-source://immich/e7ef5713-9dab-4bd4-b899-715b0ca4379e" + ) + + # get collections + item = MediaSourceItem(hass, DOMAIN, "e7ef5713-9dab-4bd4-b899-715b0ca4379e", None) + result = await source.async_browse_media(item) + + assert result + assert len(result.children) == 1 + media_file = result.children[0] + assert isinstance(media_file, BrowseMedia) + assert media_file.title == "albums" + assert media_file.media_content_id == ( + "media-source://immich/e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums" + ) + + +async def test_browse_media_get_albums( + hass: HomeAssistant, + mock_immich: Mock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test browse_media returning albums.""" + assert await async_setup_component(hass, "media_source", {}) + + with patch("homeassistant.components.immich.PLATFORMS", []): + await setup_integration(hass, mock_config_entry) + + source = await async_get_media_source(hass) + item = MediaSourceItem( + hass, DOMAIN, "e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums", None + ) + result = await source.async_browse_media(item) + + assert result + assert len(result.children) == 1 + media_file = result.children[0] + assert isinstance(media_file, BrowseMedia) + assert media_file.title == "My Album" + assert media_file.media_content_id == ( + "media-source://immich/" + "e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums/" + "721e1a4b-aa12-441e-8d3b-5ac7ab283bb6" + ) + + +async def test_browse_media_get_albums_error( hass: HomeAssistant, mock_immich: Mock, mock_config_entry: MockConfigEntry, @@ -124,7 +193,7 @@ async def test_browse_media_album_error( source = await async_get_media_source(hass) - item = MediaSourceItem(hass, DOMAIN, mock_config_entry.unique_id, None) + item = MediaSourceItem(hass, DOMAIN, f"{mock_config_entry.unique_id}/albums", None) result = await source.async_browse_media(item) assert result @@ -132,59 +201,7 @@ async def test_browse_media_album_error( assert len(result.children) == 0 -async def test_browse_media_get_root( - hass: HomeAssistant, - mock_immich: Mock, - mock_config_entry: MockConfigEntry, -) -> None: - """Test browse_media returning root media sources.""" - assert await async_setup_component(hass, "media_source", {}) - - with patch("homeassistant.components.immich.PLATFORMS", []): - await setup_integration(hass, mock_config_entry) - - source = await async_get_media_source(hass) - item = MediaSourceItem(hass, DOMAIN, "", None) - result = await source.async_browse_media(item) - - assert result - assert len(result.children) == 1 - media_file = result.children[0] - assert isinstance(media_file, BrowseMedia) - assert media_file.title == "Someone" - assert media_file.media_content_id == ( - "media-source://immich/e7ef5713-9dab-4bd4-b899-715b0ca4379e" - ) - - -async def test_browse_media_get_albums( - hass: HomeAssistant, - mock_immich: Mock, - mock_config_entry: MockConfigEntry, -) -> None: - """Test browse_media returning albums.""" - assert await async_setup_component(hass, "media_source", {}) - - with patch("homeassistant.components.immich.PLATFORMS", []): - await setup_integration(hass, mock_config_entry) - - source = await async_get_media_source(hass) - item = MediaSourceItem(hass, DOMAIN, "e7ef5713-9dab-4bd4-b899-715b0ca4379e", None) - result = await source.async_browse_media(item) - - assert result - assert len(result.children) == 1 - media_file = result.children[0] - assert isinstance(media_file, BrowseMedia) - assert media_file.title == "My Album" - assert media_file.media_content_id == ( - "media-source://immich/" - "e7ef5713-9dab-4bd4-b899-715b0ca4379e/" - "721e1a4b-aa12-441e-8d3b-5ac7ab283bb6" - ) - - -async def test_browse_media_get_items_error( +async def test_browse_media_get_album_items_error( hass: HomeAssistant, mock_immich: Mock, mock_config_entry: MockConfigEntry, @@ -202,7 +219,7 @@ async def test_browse_media_get_items_error( item = MediaSourceItem( hass, DOMAIN, - "e7ef5713-9dab-4bd4-b899-715b0ca4379e/721e1a4b-aa12-441e-8d3b-5ac7ab283bb6", + "e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums/721e1a4b-aa12-441e-8d3b-5ac7ab283bb6", None, ) result = await source.async_browse_media(item) @@ -223,7 +240,7 @@ async def test_browse_media_get_items_error( item = MediaSourceItem( hass, DOMAIN, - "e7ef5713-9dab-4bd4-b899-715b0ca4379e/721e1a4b-aa12-441e-8d3b-5ac7ab283bb6", + "e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums/721e1a4b-aa12-441e-8d3b-5ac7ab283bb6", None, ) result = await source.async_browse_media(item) @@ -233,7 +250,7 @@ async def test_browse_media_get_items_error( assert len(result.children) == 0 -async def test_browse_media_get_items( +async def test_browse_media_get_album_items( hass: HomeAssistant, mock_immich: Mock, mock_config_entry: MockConfigEntry, @@ -249,7 +266,7 @@ async def test_browse_media_get_items( item = MediaSourceItem( hass, DOMAIN, - "e7ef5713-9dab-4bd4-b899-715b0ca4379e/721e1a4b-aa12-441e-8d3b-5ac7ab283bb6", + "e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums/721e1a4b-aa12-441e-8d3b-5ac7ab283bb6", None, ) result = await source.async_browse_media(item) @@ -259,7 +276,7 @@ async def test_browse_media_get_items( media_file = result.children[0] assert isinstance(media_file, BrowseMedia) assert media_file.identifier == ( - "e7ef5713-9dab-4bd4-b899-715b0ca4379e/" + "e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums/" "721e1a4b-aa12-441e-8d3b-5ac7ab283bb6/" "2e94c203-50aa-4ad2-8e29-56dd74e0eff4/filename.jpg" ) @@ -276,7 +293,7 @@ async def test_browse_media_get_items( media_file = result.children[1] assert isinstance(media_file, BrowseMedia) assert media_file.identifier == ( - "e7ef5713-9dab-4bd4-b899-715b0ca4379e/" + "e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums/" "721e1a4b-aa12-441e-8d3b-5ac7ab283bb6/" "2e65a5f2-db83-44c4-81ab-f5ff20c9bd7b/filename.mp4" ) From 61823ec7e2f9eb840eadbbe4f7d7a6a9ca85d1df Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 27 May 2025 22:17:34 +0200 Subject: [PATCH 025/266] Fix dns resolver error in dnsip config flow validation (#145735) Fix dns resolver error in dnsip --- homeassistant/components/dnsip/config_flow.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/dnsip/config_flow.py b/homeassistant/components/dnsip/config_flow.py index e7b60d5bd6f..6b86f1627bc 100644 --- a/homeassistant/components/dnsip/config_flow.py +++ b/homeassistant/components/dnsip/config_flow.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio import contextlib -from typing import Any +from typing import Any, Literal import aiodns from aiodns.error import DNSError @@ -62,16 +62,16 @@ async def async_validate_hostname( """Validate hostname.""" async def async_check( - hostname: str, resolver: str, qtype: str, port: int = 53 + hostname: str, resolver: str, qtype: Literal["A", "AAAA"], port: int = 53 ) -> bool: """Return if able to resolve hostname.""" - result = False + result: bool = False with contextlib.suppress(DNSError): - result = bool( - await aiodns.DNSResolver( # type: ignore[call-overload] - nameservers=[resolver], udp_port=port, tcp_port=port - ).query(hostname, qtype) + _resolver = aiodns.DNSResolver( + nameservers=[resolver], udp_port=port, tcp_port=port ) + result = bool(await _resolver.query(hostname, qtype)) + return result result: dict[str, bool] = {} From e825bd0bdbbd060192fe6211eda515f5d05fb8f6 Mon Sep 17 00:00:00 2001 From: Raphael Hehl <7577984+RaHehl@users.noreply.github.com> Date: Tue, 27 May 2025 22:58:46 +0200 Subject: [PATCH 026/266] Bump uiprotect to version 7.10.1 (#145737) Co-authored-by: Jan Bouwhuis --- 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 f825e0a5eaf..1cf2e4391e2 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.10.0", "unifi-discovery==1.2.0"], + "requirements": ["uiprotect==7.10.1", "unifi-discovery==1.2.0"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index a54a12ee9c4..c885e71a969 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2987,7 +2987,7 @@ typedmonarchmoney==0.4.4 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==7.10.0 +uiprotect==7.10.1 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f381de177dd..0ea08f302a1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2422,7 +2422,7 @@ typedmonarchmoney==0.4.4 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==7.10.0 +uiprotect==7.10.1 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From fb833965221e625f16a383fdc29e69f31c57887c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Wed, 28 May 2025 14:56:47 +0100 Subject: [PATCH 027/266] Add Shelly zwave virtual integration (#145749) --- homeassistant/brands/shelly.json | 6 ++++++ homeassistant/generated/integrations.json | 16 ++++++++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) create mode 100644 homeassistant/brands/shelly.json diff --git a/homeassistant/brands/shelly.json b/homeassistant/brands/shelly.json new file mode 100644 index 00000000000..94d683157ee --- /dev/null +++ b/homeassistant/brands/shelly.json @@ -0,0 +1,6 @@ +{ + "domain": "shelly", + "name": "shelly", + "integrations": ["shelly"], + "iot_standards": ["zwave"] +} diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 4ae336f3c61..775272f77c4 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -5867,10 +5867,18 @@ "iot_class": "local_push" }, "shelly": { - "name": "Shelly", - "integration_type": "device", - "config_flow": true, - "iot_class": "local_push" + "name": "shelly", + "integrations": { + "shelly": { + "integration_type": "device", + "config_flow": true, + "iot_class": "local_push", + "name": "Shelly" + } + }, + "iot_standards": [ + "zwave" + ] }, "shodan": { "name": "Shodan", From 644a6f5569b8c8feddce1578f0fcff90bb8c0548 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 28 May 2025 08:43:59 +0200 Subject: [PATCH 028/266] Add more Amazon Devices DHCP matches (#145754) --- homeassistant/components/amazon_devices/manifest.json | 4 +++- homeassistant/generated/dhcp.py | 8 ++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/amazon_devices/manifest.json b/homeassistant/components/amazon_devices/manifest.json index 606dec83150..7593fbd4943 100644 --- a/homeassistant/components/amazon_devices/manifest.json +++ b/homeassistant/components/amazon_devices/manifest.json @@ -13,6 +13,7 @@ { "macaddress": "50D45C*" }, { "macaddress": "50DCE7*" }, { "macaddress": "68F63B*" }, + { "macaddress": "6C0C9A*" }, { "macaddress": "74D637*" }, { "macaddress": "7C6166*" }, { "macaddress": "901195*" }, @@ -22,7 +23,8 @@ { "macaddress": "A8E621*" }, { "macaddress": "C095CF*" }, { "macaddress": "D8BE65*" }, - { "macaddress": "EC2BEB*" } + { "macaddress": "EC2BEB*" }, + { "macaddress": "F02F9E*" } ], "documentation": "https://www.home-assistant.io/integrations/amazon_devices", "integration_type": "hub", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 19fa6cc706a..6ef3051a953 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -62,6 +62,10 @@ DHCP: Final[list[dict[str, str | bool]]] = [ "domain": "amazon_devices", "macaddress": "68F63B*", }, + { + "domain": "amazon_devices", + "macaddress": "6C0C9A*", + }, { "domain": "amazon_devices", "macaddress": "74D637*", @@ -102,6 +106,10 @@ DHCP: Final[list[dict[str, str | bool]]] = [ "domain": "amazon_devices", "macaddress": "EC2BEB*", }, + { + "domain": "amazon_devices", + "macaddress": "F02F9E*", + }, { "domain": "august", "hostname": "connect", From 6d44daf599c7e969314506e84f1f0e2f62735a01 Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Wed, 28 May 2025 09:39:33 +0200 Subject: [PATCH 029/266] Bump pylamarzocco to 2.0.7 (#145763) --- 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 6118e364c15..36a0a489e30 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.6"] + "requirements": ["pylamarzocco==2.0.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index c885e71a969..d8ffc983546 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2096,7 +2096,7 @@ pykwb==0.0.8 pylacrosse==0.4 # homeassistant.components.lamarzocco -pylamarzocco==2.0.6 +pylamarzocco==2.0.7 # homeassistant.components.lastfm pylast==5.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0ea08f302a1..8cbe9918eab 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1714,7 +1714,7 @@ pykrakenapi==0.1.8 pykulersky==0.5.8 # homeassistant.components.lamarzocco -pylamarzocco==2.0.6 +pylamarzocco==2.0.7 # homeassistant.components.lastfm pylast==5.1.0 From f1ec0b2c596f25b3e84f0876d20669c9a7689886 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 28 May 2025 12:26:28 +0200 Subject: [PATCH 030/266] Handle late abort when creating subentry (#145765) * Handle late abort when creating subentry * Move error handling to the base class * Narrow down expected error in test --- homeassistant/data_entry_flow.py | 13 ++- .../components/config/test_config_entries.py | 82 +++++++++++++++++++ tests/test_config_entries.py | 2 +- tests/test_data_entry_flow.py | 31 ++++++- 4 files changed, 123 insertions(+), 5 deletions(-) diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 9286f9c78f5..ce1c0806b14 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -543,8 +543,17 @@ class FlowManager(abc.ABC, Generic[_FlowContextT, _FlowResultT, _HandlerT]): flow.cur_step = result return result - # We pass a copy of the result because we're mutating our version - result = await self.async_finish_flow(flow, result.copy()) + try: + # We pass a copy of the result because we're mutating our version + result = await self.async_finish_flow(flow, result.copy()) + except AbortFlow as err: + result = self._flow_result( + type=FlowResultType.ABORT, + flow_id=flow.flow_id, + handler=flow.handler, + reason=err.reason, + description_placeholders=err.description_placeholders, + ) # _async_finish_flow may change result type, check it again if result["type"] == FlowResultType.FORM: diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 6784866ea4b..c6e82976bf1 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -1526,6 +1526,88 @@ async def test_subentry_reconfigure_flow(hass: HomeAssistant, client) -> None: } +async def test_subentry_flow_abort_duplicate(hass: HomeAssistant, client) -> None: + """Test we can handle a subentry flow raising due to unique_id collision.""" + + class TestFlow(core_ce.ConfigFlow): + class SubentryFlowHandler(core_ce.ConfigSubentryFlow): + async def async_step_user(self, user_input=None): + return await self.async_step_finish() + + async def async_step_finish(self, user_input=None): + if user_input: + return self.async_create_entry( + title="Mock title", data=user_input, unique_id="test" + ) + + return self.async_show_form( + step_id="finish", data_schema=vol.Schema({"enabled": bool}) + ) + + @classmethod + @callback + def async_get_supported_subentry_types( + cls, config_entry: core_ce.ConfigEntry + ) -> dict[str, type[core_ce.ConfigSubentryFlow]]: + return {"test": TestFlow.SubentryFlowHandler} + + mock_integration(hass, MockModule("test")) + mock_platform(hass, "test.config_flow", None) + MockConfigEntry( + domain="test", + entry_id="test1", + source="bla", + subentries_data=[ + core_ce.ConfigSubentryData( + data={}, + subentry_id="mock_id", + subentry_type="test", + title="Title", + unique_id="test", + ) + ], + ).add_to_hass(hass) + entry = hass.config_entries.async_entries()[0] + + with mock_config_flow("test", TestFlow): + url = "/api/config/config_entries/subentries/flow" + resp = await client.post(url, json={"handler": [entry.entry_id, "test"]}) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data.pop("flow_id") + assert data == { + "type": "form", + "handler": ["test1", "test"], + "step_id": "finish", + "data_schema": [{"name": "enabled", "type": "boolean"}], + "description_placeholders": None, + "errors": None, + "last_step": None, + "preview": None, + } + + with mock_config_flow("test", TestFlow): + resp = await client.post( + f"/api/config/config_entries/subentries/flow/{flow_id}", + json={"enabled": True}, + ) + assert resp.status == HTTPStatus.OK + + entries = hass.config_entries.async_entries("test") + assert len(entries) == 1 + + data = await resp.json() + data.pop("flow_id") + assert data == { + "handler": ["test1", "test"], + "reason": "already_configured", + "type": "abort", + "description_placeholders": None, + } + + async def test_subentry_does_not_support_reconfigure( hass: HomeAssistant, client: TestClient ) -> None: diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index ffff19f2c46..55b8434160e 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -2226,7 +2226,7 @@ async def test_entry_subentry_no_context( @pytest.mark.parametrize( ("unique_id", "expected_result"), - [(None, does_not_raise()), ("test", pytest.raises(HomeAssistantError))], + [(None, does_not_raise()), ("test", pytest.raises(data_entry_flow.AbortFlow))], ) async def test_entry_subentry_duplicate( hass: HomeAssistant, diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index 961afd69c2d..a5908f0feab 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -886,8 +886,8 @@ async def test_show_progress_fires_only_when_changed( ) # change (description placeholder) -async def test_abort_flow_exception(manager: MockFlowManager) -> None: - """Test that the AbortFlow exception works.""" +async def test_abort_flow_exception_step(manager: MockFlowManager) -> None: + """Test that the AbortFlow exception works in a step.""" @manager.mock_reg_handler("test") class TestFlow(data_entry_flow.FlowHandler): @@ -900,6 +900,33 @@ async def test_abort_flow_exception(manager: MockFlowManager) -> None: assert form["description_placeholders"] == {"placeholder": "yo"} +async def test_abort_flow_exception_finish_flow(hass: HomeAssistant) -> None: + """Test that the AbortFlow exception works when finishing a flow.""" + + class TestFlow(data_entry_flow.FlowHandler): + VERSION = 1 + + async def async_step_init(self, input): + """Return init form with one input field 'count'.""" + return self.async_create_entry(title="init", data=input) + + class FlowManager(data_entry_flow.FlowManager): + async def async_create_flow(self, handler_key, *, context, data): + """Create a test flow.""" + return TestFlow() + + async def async_finish_flow(self, flow, result): + """Raise AbortFlow.""" + raise data_entry_flow.AbortFlow("mock-reason", {"placeholder": "yo"}) + + manager = FlowManager(hass) + + form = await manager.async_init("test") + assert form["type"] == data_entry_flow.FlowResultType.ABORT + assert form["reason"] == "mock-reason" + assert form["description_placeholders"] == {"placeholder": "yo"} + + async def test_init_unknown_flow(manager: MockFlowManager) -> None: """Test that UnknownFlow is raised when async_create_flow returns None.""" From 13b487972307331a389b12d3b5af64a63f304e00 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 28 May 2025 09:58:44 +0200 Subject: [PATCH 031/266] Deprecate dlib image processing integrations (#145767) --- .../components/dlib_face_detect/__init__.py | 2 + .../dlib_face_detect/image_processing.py | 23 ++++++++++- .../components/dlib_face_identify/__init__.py | 3 ++ .../dlib_face_identify/image_processing.py | 25 ++++++++++- requirements_test_all.txt | 4 ++ tests/components/dlib_face_detect/__init__.py | 1 + .../dlib_face_detect/test_image_processing.py | 37 +++++++++++++++++ .../components/dlib_face_identify/__init__.py | 1 + .../test_image_processing.py | 41 +++++++++++++++++++ 9 files changed, 134 insertions(+), 3 deletions(-) create mode 100644 tests/components/dlib_face_detect/__init__.py create mode 100644 tests/components/dlib_face_detect/test_image_processing.py create mode 100644 tests/components/dlib_face_identify/__init__.py create mode 100644 tests/components/dlib_face_identify/test_image_processing.py diff --git a/homeassistant/components/dlib_face_detect/__init__.py b/homeassistant/components/dlib_face_detect/__init__.py index a732132955f..0de082595ea 100644 --- a/homeassistant/components/dlib_face_detect/__init__.py +++ b/homeassistant/components/dlib_face_detect/__init__.py @@ -1 +1,3 @@ """The dlib_face_detect component.""" + +DOMAIN = "dlib_face_detect" diff --git a/homeassistant/components/dlib_face_detect/image_processing.py b/homeassistant/components/dlib_face_detect/image_processing.py index 79f03ab3af7..9bd78f89653 100644 --- a/homeassistant/components/dlib_face_detect/image_processing.py +++ b/homeassistant/components/dlib_face_detect/image_processing.py @@ -11,10 +11,17 @@ from homeassistant.components.image_processing import ( ImageProcessingFaceEntity, ) from homeassistant.const import ATTR_LOCATION, CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE -from homeassistant.core import HomeAssistant, split_entity_id +from homeassistant.core import ( + DOMAIN as HOMEASSISTANT_DOMAIN, + HomeAssistant, + split_entity_id, +) 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 + PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA @@ -25,6 +32,20 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Dlib Face detection platform.""" + 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": "Dlib Face Detect", + }, + ) source: list[dict[str, str]] = config[CONF_SOURCE] add_entities( DlibFaceDetectEntity(camera[CONF_ENTITY_ID], camera.get(CONF_NAME)) diff --git a/homeassistant/components/dlib_face_identify/__init__.py b/homeassistant/components/dlib_face_identify/__init__.py index 79b9e4ec4bc..0e682d6b839 100644 --- a/homeassistant/components/dlib_face_identify/__init__.py +++ b/homeassistant/components/dlib_face_identify/__init__.py @@ -1 +1,4 @@ """The dlib_face_identify component.""" + +CONF_FACES = "faces" +DOMAIN = "dlib_face_identify" diff --git a/homeassistant/components/dlib_face_identify/image_processing.py b/homeassistant/components/dlib_face_identify/image_processing.py index c41dad863d4..c7c512c16d9 100644 --- a/homeassistant/components/dlib_face_identify/image_processing.py +++ b/homeassistant/components/dlib_face_identify/image_processing.py @@ -15,14 +15,20 @@ from homeassistant.components.image_processing import ( ImageProcessingFaceEntity, ) from homeassistant.const import ATTR_NAME, CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE -from homeassistant.core import HomeAssistant, split_entity_id +from homeassistant.core import ( + DOMAIN as HOMEASSISTANT_DOMAIN, + HomeAssistant, + split_entity_id, +) 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 CONF_FACES, DOMAIN + _LOGGER = logging.getLogger(__name__) -CONF_FACES = "faces" PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA.extend( { @@ -39,6 +45,21 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Dlib Face detection platform.""" + 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": "Dlib Face Identify", + }, + ) + confidence: float = config[CONF_CONFIDENCE] faces: dict[str, str] = config[CONF_FACES] source: list[dict[str, str]] = config[CONF_SOURCE] diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8cbe9918eab..24bb23a331a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -783,6 +783,10 @@ evolutionhttp==0.0.18 # homeassistant.components.faa_delays faadelays==2023.9.1 +# homeassistant.components.dlib_face_detect +# homeassistant.components.dlib_face_identify +# face-recognition==1.2.3 + # homeassistant.components.fastdotcom fastdotcom==0.0.3 diff --git a/tests/components/dlib_face_detect/__init__.py b/tests/components/dlib_face_detect/__init__.py new file mode 100644 index 00000000000..a732132955f --- /dev/null +++ b/tests/components/dlib_face_detect/__init__.py @@ -0,0 +1 @@ +"""The dlib_face_detect component.""" diff --git a/tests/components/dlib_face_detect/test_image_processing.py b/tests/components/dlib_face_detect/test_image_processing.py new file mode 100644 index 00000000000..e3b82a4cedf --- /dev/null +++ b/tests/components/dlib_face_detect/test_image_processing.py @@ -0,0 +1,37 @@ +"""Dlib Face Identity Image Processing Tests.""" + +from unittest.mock import Mock, patch + +from homeassistant.components.dlib_face_detect import DOMAIN as DLIB_DOMAIN +from homeassistant.components.image_processing import DOMAIN as IMAGE_PROCESSING_DOMAIN +from homeassistant.const import CONF_ENTITY_ID, CONF_PLATFORM, CONF_SOURCE +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.helpers import issue_registry as ir +from homeassistant.setup import async_setup_component + + +@patch.dict("sys.modules", face_recognition=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, + IMAGE_PROCESSING_DOMAIN, + { + IMAGE_PROCESSING_DOMAIN: [ + { + CONF_PLATFORM: DLIB_DOMAIN, + CONF_SOURCE: [ + {CONF_ENTITY_ID: "camera.test_camera"}, + ], + } + ], + }, + ) + await hass.async_block_till_done() + assert ( + HOMEASSISTANT_DOMAIN, + f"deprecated_system_packages_yaml_integration_{DLIB_DOMAIN}", + ) in issue_registry.issues diff --git a/tests/components/dlib_face_identify/__init__.py b/tests/components/dlib_face_identify/__init__.py new file mode 100644 index 00000000000..79b9e4ec4bc --- /dev/null +++ b/tests/components/dlib_face_identify/__init__.py @@ -0,0 +1 @@ +"""The dlib_face_identify component.""" diff --git a/tests/components/dlib_face_identify/test_image_processing.py b/tests/components/dlib_face_identify/test_image_processing.py new file mode 100644 index 00000000000..f914baeffb9 --- /dev/null +++ b/tests/components/dlib_face_identify/test_image_processing.py @@ -0,0 +1,41 @@ +"""Dlib Face Identity Image Processing Tests.""" + +from unittest.mock import Mock, patch + +from homeassistant.components.dlib_face_identify import ( + CONF_FACES, + DOMAIN as DLIB_DOMAIN, +) +from homeassistant.components.image_processing import DOMAIN as IMAGE_PROCESSING_DOMAIN +from homeassistant.const import CONF_ENTITY_ID, CONF_PLATFORM, CONF_SOURCE +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.helpers import issue_registry as ir +from homeassistant.setup import async_setup_component + + +@patch.dict("sys.modules", face_recognition=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, + IMAGE_PROCESSING_DOMAIN, + { + IMAGE_PROCESSING_DOMAIN: [ + { + CONF_PLATFORM: DLIB_DOMAIN, + CONF_SOURCE: [ + {CONF_ENTITY_ID: "camera.test_camera"}, + ], + CONF_FACES: {"person1": __file__}, + } + ], + }, + ) + await hass.async_block_till_done() + assert ( + HOMEASSISTANT_DOMAIN, + f"deprecated_system_packages_yaml_integration_{DLIB_DOMAIN}", + ) in issue_registry.issues From 74104cf1073bec7ee37791c7140aa33957d144e1 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 28 May 2025 11:16:08 +0200 Subject: [PATCH 032/266] Deprecate GStreamer integration (#145768) --- .../components/gstreamer/__init__.py | 2 ++ .../components/gstreamer/media_player.py | 20 +++++++++-- requirements_test_all.txt | 3 ++ tests/components/gstreamer/__init__.py | 1 + .../components/gstreamer/test_media_player.py | 34 +++++++++++++++++++ 5 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 tests/components/gstreamer/__init__.py create mode 100644 tests/components/gstreamer/test_media_player.py diff --git a/homeassistant/components/gstreamer/__init__.py b/homeassistant/components/gstreamer/__init__.py index 9fb97d25744..d24ac28f25f 100644 --- a/homeassistant/components/gstreamer/__init__.py +++ b/homeassistant/components/gstreamer/__init__.py @@ -1 +1,3 @@ """The gstreamer component.""" + +DOMAIN = "gstreamer" diff --git a/homeassistant/components/gstreamer/media_player.py b/homeassistant/components/gstreamer/media_player.py index bb78aff8faf..7d830377f1b 100644 --- a/homeassistant/components/gstreamer/media_player.py +++ b/homeassistant/components/gstreamer/media_player.py @@ -19,16 +19,18 @@ from homeassistant.components.media_player import ( async_process_play_media_url, ) from homeassistant.const import CONF_NAME, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, 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__) CONF_PIPELINE = "pipeline" -DOMAIN = "gstreamer" PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( {vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_PIPELINE): cv.string} @@ -48,6 +50,20 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Gstreamer platform.""" + 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": "GStreamer", + }, + ) name = config.get(CONF_NAME) pipeline = config.get(CONF_PIPELINE) diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 24bb23a331a..e14b9109ab7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -945,6 +945,9 @@ growattServer==1.6.0 # homeassistant.components.google_sheets gspread==5.5.0 +# homeassistant.components.gstreamer +gstreamer-player==1.1.2 + # homeassistant.components.profiler guppy3==3.1.5 diff --git a/tests/components/gstreamer/__init__.py b/tests/components/gstreamer/__init__.py new file mode 100644 index 00000000000..56369257098 --- /dev/null +++ b/tests/components/gstreamer/__init__.py @@ -0,0 +1 @@ +"""Gstreamer tests.""" diff --git a/tests/components/gstreamer/test_media_player.py b/tests/components/gstreamer/test_media_player.py new file mode 100644 index 00000000000..9fcf8eb7cfc --- /dev/null +++ b/tests/components/gstreamer/test_media_player.py @@ -0,0 +1,34 @@ +"""Tests for the Gstreamer platform.""" + +from unittest.mock import Mock, patch + +from homeassistant.components.gstreamer import DOMAIN as GSTREAMER_DOMAIN +from homeassistant.components.media_player import DOMAIN as PLATFORM_DOMAIN +from homeassistant.const import CONF_PLATFORM +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.helpers import issue_registry as ir +from homeassistant.setup import async_setup_component + + +@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, + { + PLATFORM_DOMAIN: [ + { + CONF_PLATFORM: GSTREAMER_DOMAIN, + } + ], + }, + ) + await hass.async_block_till_done() + assert ( + HOMEASSISTANT_DOMAIN, + f"deprecated_system_packages_yaml_integration_{GSTREAMER_DOMAIN}", + ) in issue_registry.issues From 3f172233871327fc80e446ca5d15b80a21f6fcbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Diego=20Rodr=C3=ADguez=20Royo?= Date: Wed, 28 May 2025 10:57:01 +0200 Subject: [PATCH 033/266] Add more information about possible hostnames at Home Connect (#145770) --- homeassistant/components/home_connect/manifest.json | 4 ++-- homeassistant/generated/dhcp.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/home_connect/manifest.json b/homeassistant/components/home_connect/manifest.json index e550d22e0ca..e8a36cd60d9 100644 --- a/homeassistant/components/home_connect/manifest.json +++ b/homeassistant/components/home_connect/manifest.json @@ -10,11 +10,11 @@ "macaddress": "C8D778*" }, { - "hostname": "(bosch|siemens)-*", + "hostname": "(balay|bosch|neff|siemens)-*", "macaddress": "68A40E*" }, { - "hostname": "siemens-*", + "hostname": "(siemens|neff)-*", "macaddress": "38B4D3*" } ], diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 6ef3051a953..0fb8b48b01c 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -367,12 +367,12 @@ DHCP: Final[list[dict[str, str | bool]]] = [ }, { "domain": "home_connect", - "hostname": "(bosch|siemens)-*", + "hostname": "(balay|bosch|neff|siemens)-*", "macaddress": "68A40E*", }, { "domain": "home_connect", - "hostname": "siemens-*", + "hostname": "(siemens|neff)-*", "macaddress": "38B4D3*", }, { From eb2728e5b981888c2dcefd628160ea7f2903cf66 Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Wed, 28 May 2025 10:15:24 +0200 Subject: [PATCH 034/266] Fix uom for prebrew numbers in lamarzocco (#145772) --- homeassistant/components/lamarzocco/number.py | 4 ++-- tests/components/lamarzocco/snapshots/test_number.ambr | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/lamarzocco/number.py b/homeassistant/components/lamarzocco/number.py index 7c4fe33a041..980a08c09ae 100644 --- a/homeassistant/components/lamarzocco/number.py +++ b/homeassistant/components/lamarzocco/number.py @@ -119,7 +119,7 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = ( key="prebrew_on", translation_key="prebrew_time_on", device_class=NumberDeviceClass.DURATION, - native_unit_of_measurement=UnitOfTime.MINUTES, + native_unit_of_measurement=UnitOfTime.SECONDS, native_step=PRECISION_TENTHS, native_min_value=0, native_max_value=10, @@ -158,7 +158,7 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = ( key="prebrew_off", translation_key="prebrew_time_off", device_class=NumberDeviceClass.DURATION, - native_unit_of_measurement=UnitOfTime.MINUTES, + native_unit_of_measurement=UnitOfTime.SECONDS, native_step=PRECISION_TENTHS, native_min_value=0, native_max_value=10, diff --git a/tests/components/lamarzocco/snapshots/test_number.ambr b/tests/components/lamarzocco/snapshots/test_number.ambr index 85892521456..5f451695443 100644 --- a/tests/components/lamarzocco/snapshots/test_number.ambr +++ b/tests/components/lamarzocco/snapshots/test_number.ambr @@ -126,7 +126,7 @@ 'min': 0, 'mode': , 'step': 0.1, - 'unit_of_measurement': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'number.mr012345_prebrew_off_time', @@ -173,7 +173,7 @@ 'supported_features': 0, 'translation_key': 'prebrew_time_off', 'unique_id': 'MR012345_prebrew_off', - 'unit_of_measurement': , + 'unit_of_measurement': , }) # --- # name: test_prebrew_on[Linea Micra] @@ -185,7 +185,7 @@ 'min': 0, 'mode': , 'step': 0.1, - 'unit_of_measurement': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'number.mr012345_prebrew_on_time', @@ -232,7 +232,7 @@ 'supported_features': 0, 'translation_key': 'prebrew_time_on', 'unique_id': 'MR012345_prebrew_on', - 'unit_of_measurement': , + 'unit_of_measurement': , }) # --- # name: test_preinfusion[Linea Micra] From a53c786fe07ecd8a50c1bc627140d5a0f880ca59 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 28 May 2025 13:12:55 +0200 Subject: [PATCH 035/266] Deprecate pandora integration (#145785) --- homeassistant/components/pandora/__init__.py | 2 ++ .../components/pandora/media_player.py | 20 +++++++++++- requirements_test_all.txt | 5 +++ tests/components/pandora/__init__.py | 1 + tests/components/pandora/test_media_player.py | 31 +++++++++++++++++++ 5 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 tests/components/pandora/__init__.py create mode 100644 tests/components/pandora/test_media_player.py diff --git a/homeassistant/components/pandora/__init__.py b/homeassistant/components/pandora/__init__.py index 9664730bdab..0850b00553e 100644 --- a/homeassistant/components/pandora/__init__.py +++ b/homeassistant/components/pandora/__init__.py @@ -1 +1,3 @@ """The pandora component.""" + +DOMAIN = "pandora" diff --git a/homeassistant/components/pandora/media_player.py b/homeassistant/components/pandora/media_player.py index 064b2930971..77564245522 100644 --- a/homeassistant/components/pandora/media_player.py +++ b/homeassistant/components/pandora/media_player.py @@ -27,10 +27,13 @@ from homeassistant.const import ( SERVICE_VOLUME_DOWN, SERVICE_VOLUME_UP, ) -from homeassistant.core import Event, HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, Event, HomeAssistant 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__) @@ -53,6 +56,21 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Pandora media player platform.""" + 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": "Pandora", + }, + ) + if not _pianobar_exists(): return pandora = PandoraMediaPlayer("Pandora") diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e14b9109ab7..f03e3c6bc6a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1393,6 +1393,11 @@ peco==0.1.2 # homeassistant.components.escea pescea==1.0.12 +# homeassistant.components.aruba +# homeassistant.components.cisco_ios +# homeassistant.components.pandora +pexpect==4.9.0 + # homeassistant.components.modem_callerid phone-modem==0.1.1 diff --git a/tests/components/pandora/__init__.py b/tests/components/pandora/__init__.py new file mode 100644 index 00000000000..6fccecfd679 --- /dev/null +++ b/tests/components/pandora/__init__.py @@ -0,0 +1 @@ +"""Padora component tests.""" diff --git a/tests/components/pandora/test_media_player.py b/tests/components/pandora/test_media_player.py new file mode 100644 index 00000000000..2af72ba2224 --- /dev/null +++ b/tests/components/pandora/test_media_player.py @@ -0,0 +1,31 @@ +"""Pandora media player tests.""" + +from homeassistant.components.media_player import DOMAIN as PLATFORM_DOMAIN +from homeassistant.components.pandora import DOMAIN as PANDORA_DOMAIN +from homeassistant.const import CONF_PLATFORM +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.helpers import issue_registry as ir +from homeassistant.setup import async_setup_component + + +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, + { + PLATFORM_DOMAIN: [ + { + CONF_PLATFORM: PANDORA_DOMAIN, + } + ], + }, + ) + await hass.async_block_till_done() + assert ( + HOMEASSISTANT_DOMAIN, + f"deprecated_system_packages_yaml_integration_{PANDORA_DOMAIN}", + ) in issue_registry.issues From fbd05a0fcf3e833c5410a75eaa0cf6cd9030a9f8 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 28 May 2025 18:28:37 +0200 Subject: [PATCH 036/266] Deprecate lirc integration (#145797) --- homeassistant/components/lirc/__init__.py | 17 ++++++++++++- requirements_test_all.txt | 3 +++ tests/components/lirc/__init__.py | 1 + tests/components/lirc/test_init.py | 31 +++++++++++++++++++++++ 4 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 tests/components/lirc/__init__.py create mode 100644 tests/components/lirc/test_init.py diff --git a/homeassistant/components/lirc/__init__.py b/homeassistant/components/lirc/__init__.py index f5b26743a03..6b8e0d08d52 100644 --- a/homeassistant/components/lirc/__init__.py +++ b/homeassistant/components/lirc/__init__.py @@ -7,8 +7,9 @@ import time import lirc from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.issue_registry import IssueSeverity, create_issue from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) @@ -26,6 +27,20 @@ CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) def setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the LIRC capability.""" + 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": "LIRC", + }, + ) # blocking=True gives unexpected behavior (multiple responses for 1 press) # also by not blocking, we allow hass to shut down the thread gracefully # on exit. diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f03e3c6bc6a..a67f27dc6b5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2012,6 +2012,9 @@ python-kasa[speedups]==0.10.2 # homeassistant.components.linkplay python-linkplay==0.2.8 +# homeassistant.components.lirc +# python-lirc==1.2.3 + # homeassistant.components.matter python-matter-server==7.0.0 diff --git a/tests/components/lirc/__init__.py b/tests/components/lirc/__init__.py new file mode 100644 index 00000000000..f8e11b194a6 --- /dev/null +++ b/tests/components/lirc/__init__.py @@ -0,0 +1 @@ +"""LIRC tests.""" diff --git a/tests/components/lirc/test_init.py b/tests/components/lirc/test_init.py new file mode 100644 index 00000000000..d6fd7975c77 --- /dev/null +++ b/tests/components/lirc/test_init.py @@ -0,0 +1,31 @@ +"""Tests for the LIRC.""" + +from unittest.mock import Mock, patch + +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.helpers import issue_registry as ir +from homeassistant.setup import async_setup_component + + +@patch.dict("sys.modules", lirc=Mock()) +async def test_repair_issue_is_created( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, +) -> None: + """Test repair issue is created.""" + from homeassistant.components.lirc import ( # pylint: disable=import-outside-toplevel + DOMAIN as LIRC_DOMAIN, + ) + + assert await async_setup_component( + hass, + LIRC_DOMAIN, + { + LIRC_DOMAIN: {}, + }, + ) + await hass.async_block_till_done() + assert ( + HOMEASSISTANT_DOMAIN, + f"deprecated_system_packages_yaml_integration_{LIRC_DOMAIN}", + ) in issue_registry.issues From 74102d03197f32e40773872fc4628c77c65a6ab1 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 28 May 2025 16:33:20 +0200 Subject: [PATCH 037/266] Bump reolink-aio to 0.13.4 (#145799) --- 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 a6f0b59426a..694dd43a532 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -19,5 +19,5 @@ "iot_class": "local_push", "loggers": ["reolink_aio"], "quality_scale": "platinum", - "requirements": ["reolink-aio==0.13.3"] + "requirements": ["reolink-aio==0.13.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index d8ffc983546..da5d520c1a3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2652,7 +2652,7 @@ renault-api==0.3.1 renson-endura-delta==1.7.2 # homeassistant.components.reolink -reolink-aio==0.13.3 +reolink-aio==0.13.4 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a67f27dc6b5..9cb25847a65 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2180,7 +2180,7 @@ renault-api==0.3.1 renson-endura-delta==1.7.2 # homeassistant.components.reolink -reolink-aio==0.13.3 +reolink-aio==0.13.4 # homeassistant.components.rflink rflink==0.0.66 From 83af5ec36bed410748e6574cb0f14c692c0f941f Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 28 May 2025 17:22:18 +0200 Subject: [PATCH 038/266] Deprecate keyboard integration (#145805) --- homeassistant/components/keyboard/__init__.py | 17 ++++++++++- requirements_test_all.txt | 3 ++ tests/components/keyboard/__init__.py | 1 + tests/components/keyboard/test_init.py | 29 +++++++++++++++++++ 4 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 tests/components/keyboard/__init__.py create mode 100644 tests/components/keyboard/test_init.py diff --git a/homeassistant/components/keyboard/__init__.py b/homeassistant/components/keyboard/__init__.py index bf935f119d0..227472ff553 100644 --- a/homeassistant/components/keyboard/__init__.py +++ b/homeassistant/components/keyboard/__init__.py @@ -11,8 +11,9 @@ from homeassistant.const import ( SERVICE_VOLUME_MUTE, SERVICE_VOLUME_UP, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.issue_registry import IssueSeverity, create_issue from homeassistant.helpers.typing import ConfigType DOMAIN = "keyboard" @@ -24,6 +25,20 @@ CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) def setup(hass: HomeAssistant, config: ConfigType) -> bool: """Listen for keyboard events.""" + 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": "Keyboard", + }, + ) keyboard = PyKeyboard() keyboard.special_key_assignment() diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9cb25847a65..58b27544486 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2098,6 +2098,9 @@ pytrydan==0.8.0 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 +# homeassistant.components.keyboard +# pyuserinput==0.1.11 + # homeassistant.components.vera pyvera==0.3.15 diff --git a/tests/components/keyboard/__init__.py b/tests/components/keyboard/__init__.py new file mode 100644 index 00000000000..7bc8a91511f --- /dev/null +++ b/tests/components/keyboard/__init__.py @@ -0,0 +1 @@ +"""Keyboard tests.""" diff --git a/tests/components/keyboard/test_init.py b/tests/components/keyboard/test_init.py new file mode 100644 index 00000000000..42a700a3d07 --- /dev/null +++ b/tests/components/keyboard/test_init.py @@ -0,0 +1,29 @@ +"""Keyboard tests.""" + +from unittest.mock import Mock, patch + +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.helpers import issue_registry as ir +from homeassistant.setup import async_setup_component + + +@patch.dict("sys.modules", pykeyboard=Mock()) +async def test_repair_issue_is_created( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, +) -> None: + """Test repair issue is created.""" + from homeassistant.components.keyboard import ( # pylint:disable=import-outside-toplevel + DOMAIN as KEYBOARD_DOMAIN, + ) + + assert await async_setup_component( + hass, + KEYBOARD_DOMAIN, + {KEYBOARD_DOMAIN: {}}, + ) + await hass.async_block_till_done() + assert ( + HOMEASSISTANT_DOMAIN, + f"deprecated_system_packages_yaml_integration_{KEYBOARD_DOMAIN}", + ) in issue_registry.issues From 612861061cba5302aa08173a8b025756cfce2bc1 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Wed, 28 May 2025 17:52:51 +0100 Subject: [PATCH 039/266] Fix HOMEASSISTANT_STOP unsubscribe in data update coordinator (#145809) * initial commit * a better approach * Add comment --- homeassistant/helpers/update_coordinator.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index 7130264eb0d..bd85391f98f 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -138,6 +138,8 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]): async def _on_hass_stop(_: Event) -> None: """Shutdown coordinator on HomeAssistant stop.""" + # Already cleared on EVENT_HOMEASSISTANT_STOP, via async_fire_internal + self._unsub_shutdown = None await self.async_shutdown() self._unsub_shutdown = self.hass.bus.async_listen_once( From 12f8ebb3eae35ab945b82442461d4baedec2d5ae Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 28 May 2025 13:14:13 -0500 Subject: [PATCH 040/266] Bump intents to 2025.5.28 (#145816) --- homeassistant/components/conversation/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/hassfest/docker/Dockerfile | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 2955bb96833..6078d73e99b 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.5.7"] + "requirements": ["hassil==2.2.3", "home-assistant-intents==2025.5.28"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 378e9fdce83..c2c4a20b947 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -39,7 +39,7 @@ hass-nabucasa==0.101.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 home-assistant-frontend==20250527.0 -home-assistant-intents==2025.5.7 +home-assistant-intents==2025.5.28 httpx==0.28.1 ifaddr==0.2.0 Jinja2==3.1.6 diff --git a/pyproject.toml b/pyproject.toml index 7e1e98bdb24..78d09a61477 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,7 +66,7 @@ dependencies = [ # onboarding->cloud->assist_pipeline->conversation->home_assistant_intents. Onboarding needs # to be setup in stage 0, but we don't want to also promote cloud with all its # dependencies to stage 0. - "home-assistant-intents==2025.5.7", + "home-assistant-intents==2025.5.28", "ifaddr==0.2.0", "Jinja2==3.1.6", "lru-dict==1.3.0", diff --git a/requirements.txt b/requirements.txt index e4d1cc5ba30..403948d1445 100644 --- a/requirements.txt +++ b/requirements.txt @@ -27,7 +27,7 @@ hass-nabucasa==0.101.0 hassil==2.2.3 httpx==0.28.1 home-assistant-bluetooth==1.13.1 -home-assistant-intents==2025.5.7 +home-assistant-intents==2025.5.28 ifaddr==0.2.0 Jinja2==3.1.6 lru-dict==1.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index da5d520c1a3..2716c6992cb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1167,7 +1167,7 @@ holidays==0.73 home-assistant-frontend==20250527.0 # homeassistant.components.conversation -home-assistant-intents==2025.5.7 +home-assistant-intents==2025.5.28 # homeassistant.components.homematicip_cloud homematicip==2.0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 58b27544486..ea2df4c3fe0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1004,7 +1004,7 @@ holidays==0.73 home-assistant-frontend==20250527.0 # homeassistant.components.conversation -home-assistant-intents==2025.5.7 +home-assistant-intents==2025.5.28 # homeassistant.components.homematicip_cloud homematicip==2.0.1.1 diff --git a/script/hassfest/docker/Dockerfile b/script/hassfest/docker/Dockerfile index 5ca638ef487..647755d8237 100644 --- a/script/hassfest/docker/Dockerfile +++ b/script/hassfest/docker/Dockerfile @@ -25,7 +25,7 @@ RUN --mount=from=ghcr.io/astral-sh/uv:0.7.1,source=/uv,target=/bin/uv \ -c /usr/src/homeassistant/homeassistant/package_constraints.txt \ -r /usr/src/homeassistant/requirements.txt \ stdlib-list==0.10.0 pipdeptree==2.26.1 tqdm==4.67.1 ruff==0.11.0 \ - PyTurboJPEG==1.7.5 go2rtc-client==0.1.3b0 ha-ffmpeg==3.2.2 hassil==2.2.3 home-assistant-intents==2025.5.7 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2 + PyTurboJPEG==1.7.5 go2rtc-client==0.1.3b0 ha-ffmpeg==3.2.2 hassil==2.2.3 home-assistant-intents==2025.5.28 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2 LABEL "name"="hassfest" LABEL "maintainer"="Home Assistant " From 309acb961b4b1b223bf3822c20fdaf9db3739f6e Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 28 May 2025 20:49:20 +0200 Subject: [PATCH 041/266] Fix Immich media source browsing with multiple config entries (#145823) fix media source browsing with multiple config entries --- homeassistant/components/immich/media_source.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/immich/media_source.py b/homeassistant/components/immich/media_source.py index 0d0616875c6..9304039f297 100644 --- a/homeassistant/components/immich/media_source.py +++ b/homeassistant/components/immich/media_source.py @@ -30,11 +30,8 @@ LOGGER = getLogger(__name__) async def async_get_media_source(hass: HomeAssistant) -> MediaSource: """Set up Immich media source.""" - entries = hass.config_entries.async_entries( - DOMAIN, include_disabled=False, include_ignore=False - ) hass.http.register_view(ImmichMediaView(hass)) - return ImmichMediaSource(hass, entries) + return ImmichMediaSource(hass) class ImmichMediaSourceIdentifier: @@ -56,18 +53,17 @@ class ImmichMediaSource(MediaSource): name = "Immich" - def __init__(self, hass: HomeAssistant, entries: list[ConfigEntry]) -> None: + def __init__(self, hass: HomeAssistant) -> None: """Initialize Immich media source.""" super().__init__(DOMAIN) self.hass = hass - self.entries = entries async def async_browse_media( self, item: MediaSourceItem, ) -> BrowseMediaSource: """Return media.""" - if not self.hass.config_entries.async_loaded_entries(DOMAIN): + if not (entries := self.hass.config_entries.async_loaded_entries(DOMAIN)): raise BrowseError("Immich is not configured") return BrowseMediaSource( domain=DOMAIN, @@ -79,12 +75,12 @@ class ImmichMediaSource(MediaSource): can_expand=True, children_media_class=MediaClass.DIRECTORY, children=[ - *await self._async_build_immich(item), + *await self._async_build_immich(item, entries), ], ) async def _async_build_immich( - self, item: MediaSourceItem + self, item: MediaSourceItem, entries: list[ConfigEntry] ) -> list[BrowseMediaSource]: """Handle browsing different immich instances.""" if not item.identifier: @@ -99,7 +95,7 @@ class ImmichMediaSource(MediaSource): can_play=False, can_expand=True, ) - for entry in self.entries + for entry in entries ] identifier = ImmichMediaSourceIdentifier(item.identifier) entry: ImmichConfigEntry | None = ( From d0d228d9f4dac8cb1535b45a5580921f6ea85e9f Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 28 May 2025 23:17:14 +0200 Subject: [PATCH 042/266] Update frontend to 20250528.0 (#145828) Co-authored-by: Robert Resch --- 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 32d243b3431..3ee40e1ce60 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==20250527.0"] + "requirements": ["home-assistant-frontend==20250528.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c2c4a20b947..33766ec52c4 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ habluetooth==3.48.2 hass-nabucasa==0.101.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250527.0 +home-assistant-frontend==20250528.0 home-assistant-intents==2025.5.28 httpx==0.28.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 2716c6992cb..2677eedbb39 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1164,7 +1164,7 @@ hole==0.8.0 holidays==0.73 # homeassistant.components.frontend -home-assistant-frontend==20250527.0 +home-assistant-frontend==20250528.0 # homeassistant.components.conversation home-assistant-intents==2025.5.28 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ea2df4c3fe0..185e51d8a48 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1001,7 +1001,7 @@ hole==0.8.0 holidays==0.73 # homeassistant.components.frontend -home-assistant-frontend==20250527.0 +home-assistant-frontend==20250528.0 # homeassistant.components.conversation home-assistant-intents==2025.5.28 From 17a0b4f3d0942f5bd6f60f8da2d87db05579f79e Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 28 May 2025 23:18:38 +0200 Subject: [PATCH 043/266] Bump version to 2025.6.0b2 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 6b3cbb4f27c..5a6ac701771 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 = 6 -PATCH_VERSION: Final = "0b1" +PATCH_VERSION: Final = "0b2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index 78d09a61477..04dcb3e3057 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.6.0b1" +version = "2025.6.0b2" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From e0d3b819e5153073e69321318e52a6e6301f5d00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Lersveen?= <7195448+lersveen@users.noreply.github.com> Date: Thu, 29 May 2025 03:52:05 +0200 Subject: [PATCH 044/266] Set correct nobo_hub max temperature (#145751) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Max temperature 30°C is implemented upstream in pynobo and the Nobø Energy Hub app also stops at 30°C. --- homeassistant/components/nobo_hub/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/nobo_hub/climate.py b/homeassistant/components/nobo_hub/climate.py index 771da420213..018f3e2b06a 100644 --- a/homeassistant/components/nobo_hub/climate.py +++ b/homeassistant/components/nobo_hub/climate.py @@ -40,7 +40,7 @@ SUPPORT_FLAGS = ( PRESET_MODES = [PRESET_NONE, PRESET_COMFORT, PRESET_ECO, PRESET_AWAY] MIN_TEMPERATURE = 7 -MAX_TEMPERATURE = 40 +MAX_TEMPERATURE = 30 async def async_setup_entry( From fa66ea31d398af81f14f2a12aa31a49a5550e603 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Thu, 29 May 2025 15:35:35 +0200 Subject: [PATCH 045/266] Deprecate tensorflow (#145806) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Abílio Costa --- .../components/tensorflow/__init__.py | 3 ++ .../components/tensorflow/image_processing.py | 26 ++++++++++-- requirements_test_all.txt | 9 +++++ tests/components/tensorflow/__init__.py | 1 + .../tensorflow/test_image_processing.py | 40 +++++++++++++++++++ 5 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 tests/components/tensorflow/__init__.py create mode 100644 tests/components/tensorflow/test_image_processing.py diff --git a/homeassistant/components/tensorflow/__init__.py b/homeassistant/components/tensorflow/__init__.py index 00a695d6aa8..7ed20cbe4b6 100644 --- a/homeassistant/components/tensorflow/__init__.py +++ b/homeassistant/components/tensorflow/__init__.py @@ -1 +1,4 @@ """The tensorflow component.""" + +DOMAIN = "tensorflow" +CONF_GRAPH = "graph" diff --git a/homeassistant/components/tensorflow/image_processing.py b/homeassistant/components/tensorflow/image_processing.py index 0fb069e8da8..05be56d444d 100644 --- a/homeassistant/components/tensorflow/image_processing.py +++ b/homeassistant/components/tensorflow/image_processing.py @@ -26,15 +26,21 @@ from homeassistant.const import ( CONF_SOURCE, EVENT_HOMEASSISTANT_START, ) -from homeassistant.core import HomeAssistant, split_entity_id +from homeassistant.core import ( + DOMAIN as HOMEASSISTANT_DOMAIN, + HomeAssistant, + split_entity_id, +) from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util.pil import draw_box +from . import CONF_GRAPH, DOMAIN + os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2" -DOMAIN = "tensorflow" _LOGGER = logging.getLogger(__name__) ATTR_MATCHES = "matches" @@ -47,7 +53,6 @@ CONF_BOTTOM = "bottom" CONF_CATEGORIES = "categories" CONF_CATEGORY = "category" CONF_FILE_OUT = "file_out" -CONF_GRAPH = "graph" CONF_LABELS = "labels" CONF_LABEL_OFFSET = "label_offset" CONF_LEFT = "left" @@ -110,6 +115,21 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the TensorFlow image processing platform.""" + 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": "Tensorflow", + }, + ) + model_config = config[CONF_MODEL] model_dir = model_config.get(CONF_MODEL_DIR) or hass.config.path("tensorflow") labels = model_config.get(CONF_LABELS) or hass.config.path( diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 185e51d8a48..a5a927e94bd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1560,6 +1560,9 @@ pybravia==0.3.4 # homeassistant.components.cloudflare pycfdns==3.0.0 +# homeassistant.components.tensorflow +# pycocotools==2.0.6 + # homeassistant.components.comfoconnect pycomfoconnect==0.5.1 @@ -2365,6 +2368,9 @@ temescal==0.5 # homeassistant.components.temper temperusb==1.6.1 +# homeassistant.components.tensorflow +# tensorflow==2.5.0 + # homeassistant.components.tesla_fleet # homeassistant.components.teslemetry # homeassistant.components.tessie @@ -2382,6 +2388,9 @@ teslemetry-stream==0.7.9 # homeassistant.components.tessie tessie-api==0.1.1 +# homeassistant.components.tensorflow +# tf-models-official==2.5.0 + # homeassistant.components.thermobeacon thermobeacon-ble==0.10.0 diff --git a/tests/components/tensorflow/__init__.py b/tests/components/tensorflow/__init__.py new file mode 100644 index 00000000000..458de30c9fa --- /dev/null +++ b/tests/components/tensorflow/__init__.py @@ -0,0 +1 @@ +"""TensorFlow component tests.""" diff --git a/tests/components/tensorflow/test_image_processing.py b/tests/components/tensorflow/test_image_processing.py new file mode 100644 index 00000000000..06199b9c60c --- /dev/null +++ b/tests/components/tensorflow/test_image_processing.py @@ -0,0 +1,40 @@ +"""Tensorflow test.""" + +from unittest.mock import Mock, patch + +from homeassistant.components.image_processing import DOMAIN as IMAGE_PROCESSING_DOMAINN +from homeassistant.components.tensorflow import CONF_GRAPH, DOMAIN as TENSORFLOW_DOMAIN +from homeassistant.const import CONF_ENTITY_ID, CONF_MODEL, CONF_PLATFORM, CONF_SOURCE +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.helpers import issue_registry as ir +from homeassistant.setup import async_setup_component + + +@patch.dict("sys.modules", tensorflow=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, + IMAGE_PROCESSING_DOMAINN, + { + IMAGE_PROCESSING_DOMAINN: [ + { + CONF_PLATFORM: TENSORFLOW_DOMAIN, + CONF_SOURCE: [ + {CONF_ENTITY_ID: "camera.test_camera"}, + ], + CONF_MODEL: { + CONF_GRAPH: ".", + }, + } + ], + }, + ) + await hass.async_block_till_done() + assert ( + HOMEASSISTANT_DOMAIN, + f"deprecated_system_packages_yaml_integration_{TENSORFLOW_DOMAIN}", + ) in issue_registry.issues From 95fb2a7d7f466e15a7916114aa31d9f16ca677d5 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 28 May 2025 23:54:48 +0200 Subject: [PATCH 046/266] Deprecate decora integration (#145807) --- homeassistant/components/decora/__init__.py | 2 ++ homeassistant/components/decora/light.py | 19 ++++++++++++ requirements_test_all.txt | 6 ++++ tests/components/decora/__init__.py | 1 + tests/components/decora/test_light.py | 34 +++++++++++++++++++++ 5 files changed, 62 insertions(+) create mode 100644 tests/components/decora/__init__.py create mode 100644 tests/components/decora/test_light.py diff --git a/homeassistant/components/decora/__init__.py b/homeassistant/components/decora/__init__.py index 694ff77fdb3..4ba4fb4dee0 100644 --- a/homeassistant/components/decora/__init__.py +++ b/homeassistant/components/decora/__init__.py @@ -1 +1,3 @@ """The decora component.""" + +DOMAIN = "decora" diff --git a/homeassistant/components/decora/light.py b/homeassistant/components/decora/light.py index a7d14b83aca..d0226a24dcc 100644 --- a/homeassistant/components/decora/light.py +++ b/homeassistant/components/decora/light.py @@ -21,7 +21,11 @@ from homeassistant.components.light import ( LightEntity, ) from homeassistant.const import CONF_API_KEY, CONF_DEVICES, CONF_NAME +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.issue_registry import IssueSeverity, create_issue + +from . import DOMAIN if TYPE_CHECKING: from homeassistant.core import HomeAssistant @@ -90,6 +94,21 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up an Decora switch.""" + 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": "Leviton Decora", + }, + ) + lights = [] for address, device_config in config[CONF_DEVICES].items(): device = {} diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a5a927e94bd..263832d2bc1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -561,6 +561,9 @@ bluecurrent-api==1.2.3 # homeassistant.components.bluemaestro bluemaestro-ble==0.4.1 +# homeassistant.components.decora +# bluepy==1.3.0 + # homeassistant.components.bluetooth bluetooth-adapters==0.21.4 @@ -655,6 +658,9 @@ dbus-fast==2.43.0 # homeassistant.components.debugpy debugpy==1.8.14 +# homeassistant.components.decora +# decora==0.6 + # homeassistant.components.ecovacs deebot-client==13.2.1 diff --git a/tests/components/decora/__init__.py b/tests/components/decora/__init__.py new file mode 100644 index 00000000000..399b353aa0c --- /dev/null +++ b/tests/components/decora/__init__.py @@ -0,0 +1 @@ +"""Decora component tests.""" diff --git a/tests/components/decora/test_light.py b/tests/components/decora/test_light.py new file mode 100644 index 00000000000..6315d6c3986 --- /dev/null +++ b/tests/components/decora/test_light.py @@ -0,0 +1,34 @@ +"""Decora component tests.""" + +from unittest.mock import Mock, patch + +from homeassistant.components.decora import DOMAIN as DECORA_DOMAIN +from homeassistant.components.light import DOMAIN as PLATFORM_DOMAIN +from homeassistant.const import CONF_PLATFORM +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.helpers import issue_registry as ir +from homeassistant.setup import async_setup_component + + +@patch.dict("sys.modules", {"bluepy": Mock(), "bluepy.btle": Mock(), "decora": 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, + { + PLATFORM_DOMAIN: [ + { + CONF_PLATFORM: DECORA_DOMAIN, + } + ], + }, + ) + await hass.async_block_till_done() + assert ( + HOMEASSISTANT_DOMAIN, + f"deprecated_system_packages_yaml_integration_{DECORA_DOMAIN}", + ) in issue_registry.issues From 26586b451435e02785e94cc8a3fff764e2bd6eef Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 29 May 2025 15:28:54 +0200 Subject: [PATCH 047/266] Fix language selections in workday (#145813) --- .../components/workday/binary_sensor.py | 44 ++++++++++++++++-- .../components/workday/config_flow.py | 17 +------ .../components/workday/test_binary_sensor.py | 46 +++++++++++++++++++ 3 files changed, 89 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/workday/binary_sensor.py b/homeassistant/components/workday/binary_sensor.py index 6b878db8159..a48e19e59b2 100644 --- a/homeassistant/components/workday/binary_sensor.py +++ b/homeassistant/components/workday/binary_sensor.py @@ -94,21 +94,59 @@ def _get_obj_holidays( language=language, categories=set_categories, ) + + supported_languages = obj_holidays.supported_languages + default_language = obj_holidays.default_language + + if default_language and not language: + # If no language is set, use the default language + LOGGER.debug("Changing language from None to %s", default_language) + return country_holidays( # Return default if no language + country, + subdiv=province, + years=year, + language=default_language, + categories=set_categories, + ) + if ( - (supported_languages := obj_holidays.supported_languages) + default_language and language + and language not in supported_languages and language.startswith("en") ): + # If language does not match supported languages, use the first English variant + if default_language.startswith("en"): + LOGGER.debug("Changing language from %s to %s", language, default_language) + return country_holidays( # Return default English if default language + country, + subdiv=province, + years=year, + language=default_language, + categories=set_categories, + ) for lang in supported_languages: if lang.startswith("en"): - obj_holidays = country_holidays( + LOGGER.debug("Changing language from %s to %s", language, lang) + return country_holidays( country, subdiv=province, years=year, language=lang, categories=set_categories, ) - LOGGER.debug("Changing language from %s to %s", language, lang) + + if default_language and language and language not in supported_languages: + # If language does not match supported languages, use the default language + LOGGER.debug("Changing language from %s to %s", language, default_language) + return country_holidays( # Return default English if default language + country, + subdiv=province, + years=year, + language=default_language, + categories=set_categories, + ) + return obj_holidays diff --git a/homeassistant/components/workday/config_flow.py b/homeassistant/components/workday/config_flow.py index b0b1e9fcc02..7a8a8181a9f 100644 --- a/homeassistant/components/workday/config_flow.py +++ b/homeassistant/components/workday/config_flow.py @@ -67,8 +67,7 @@ def add_province_and_language_to_schema( _country = country_holidays(country=country) if country_default_language := (_country.default_language): - selectable_languages = _country.supported_languages - new_selectable_languages = list(selectable_languages) + new_selectable_languages = list(_country.supported_languages) language_schema = { vol.Optional( CONF_LANGUAGE, default=country_default_language @@ -154,19 +153,7 @@ def validate_custom_dates(user_input: dict[str, Any]) -> None: years=year, language=language, ) - if ( - (supported_languages := obj_holidays.supported_languages) - and language - and language.startswith("en") - ): - for lang in supported_languages: - if lang.startswith("en"): - obj_holidays = country_holidays( - country, - subdiv=province, - years=year, - language=lang, - ) + else: obj_holidays = HolidayBase(years=year) diff --git a/tests/components/workday/test_binary_sensor.py b/tests/components/workday/test_binary_sensor.py index 212c3e9d305..8f8894e3536 100644 --- a/tests/components/workday/test_binary_sensor.py +++ b/tests/components/workday/test_binary_sensor.py @@ -461,3 +461,49 @@ async def test_only_repairs_for_current_next_year( assert len(issue_registry.issues) == 2 assert issue_registry.issues == snapshot + + +async def test_missing_language( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test when language exist but is empty.""" + config = { + "add_holidays": [], + "country": "AU", + "days_offset": 0, + "excludes": ["sat", "sun", "holiday"], + "language": None, + "name": "Workday Sensor", + "platform": "workday", + "province": "QLD", + "remove_holidays": [ + "Labour Day", + ], + "workdays": ["mon", "tue", "wed", "thu", "fri"], + } + await init_integration(hass, config) + assert "Changing language from None to en_AU" in caplog.text + + +async def test_incorrect_english_variant( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test when language exist but is empty.""" + config = { + "add_holidays": [], + "country": "AU", + "days_offset": 0, + "excludes": ["sat", "sun", "holiday"], + "language": "en_UK", # Incorrect variant + "name": "Workday Sensor", + "platform": "workday", + "province": "QLD", + "remove_holidays": [ + "Labour Day", + ], + "workdays": ["mon", "tue", "wed", "thu", "fri"], + } + await init_integration(hass, config) + assert "Changing language from en_UK to en_AU" in caplog.text From 4d22b35a9f109f914a2210617b32dd24643b9ec3 Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Thu, 29 May 2025 09:35:05 +0200 Subject: [PATCH 048/266] Bump aiotedee to 0.2.23 (#145822) * Bump aiotedee to 0.2.23 * update snapshot --- homeassistant/components/tedee/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/tedee/snapshots/test_diagnostics.ambr | 2 ++ 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tedee/manifest.json b/homeassistant/components/tedee/manifest.json index bca51f08f93..012e82318ed 100644 --- a/homeassistant/components/tedee/manifest.json +++ b/homeassistant/components/tedee/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["aiotedee"], "quality_scale": "platinum", - "requirements": ["aiotedee==0.2.20"] + "requirements": ["aiotedee==0.2.23"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2677eedbb39..0aad41a4bcc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -405,7 +405,7 @@ aiosyncthing==0.5.1 aiotankerkoenig==0.4.2 # homeassistant.components.tedee -aiotedee==0.2.20 +aiotedee==0.2.23 # homeassistant.components.tractive aiotractive==0.6.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 263832d2bc1..d358800b7f4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -387,7 +387,7 @@ aiosyncthing==0.5.1 aiotankerkoenig==0.4.2 # homeassistant.components.tedee -aiotedee==0.2.20 +aiotedee==0.2.23 # homeassistant.components.tractive aiotractive==0.6.0 diff --git a/tests/components/tedee/snapshots/test_diagnostics.ambr b/tests/components/tedee/snapshots/test_diagnostics.ambr index 401c519c215..046a8fd210a 100644 --- a/tests/components/tedee/snapshots/test_diagnostics.ambr +++ b/tests/components/tedee/snapshots/test_diagnostics.ambr @@ -6,6 +6,7 @@ 'duration_pullspring': 2, 'is_charging': False, 'is_connected': True, + 'is_enabled_auto_pullspring': False, 'is_enabled_pullspring': 1, 'lock_id': '**REDACTED**', 'lock_name': 'Lock-1A2B', @@ -18,6 +19,7 @@ 'duration_pullspring': 0, 'is_charging': False, 'is_connected': True, + 'is_enabled_auto_pullspring': False, 'is_enabled_pullspring': 0, 'lock_id': '**REDACTED**', 'lock_name': 'Lock-2C3D', From 0e87d14ca8890c7427432e37914e8588b2fb45d3 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Thu, 29 May 2025 10:28:02 +0200 Subject: [PATCH 049/266] Use mime type provided by Immich (#145830) use mime type from immich instead of guessing it --- .../components/immich/media_source.py | 68 ++++++++++-------- tests/components/immich/test_media_source.py | 69 +++++++++++-------- 2 files changed, 78 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/immich/media_source.py b/homeassistant/components/immich/media_source.py index 9304039f297..a7c55f9c572 100644 --- a/homeassistant/components/immich/media_source.py +++ b/homeassistant/components/immich/media_source.py @@ -3,7 +3,6 @@ from __future__ import annotations from logging import getLogger -import mimetypes from aiohttp.web import HTTPNotFound, Request, Response, StreamResponse from aioimmich.exceptions import ImmichError @@ -39,13 +38,14 @@ class ImmichMediaSourceIdentifier: def __init__(self, identifier: str) -> None: """Split identifier into parts.""" - parts = identifier.split("/") - # config_entry.unique_id/collection/collection_id/asset_id/file_name + parts = identifier.split("|") + # config_entry.unique_id|collection|collection_id|asset_id|file_name|mime_type self.unique_id = parts[0] self.collection = parts[1] if len(parts) > 1 else None self.collection_id = parts[2] if len(parts) > 2 else None self.asset_id = parts[3] if len(parts) > 3 else None self.file_name = parts[4] if len(parts) > 3 else None + self.mime_type = parts[5] if len(parts) > 3 else None class ImmichMediaSource(MediaSource): @@ -111,7 +111,7 @@ class ImmichMediaSource(MediaSource): return [ BrowseMediaSource( domain=DOMAIN, - identifier=f"{identifier.unique_id}/albums", + identifier=f"{identifier.unique_id}|albums", media_class=MediaClass.DIRECTORY, media_content_type=MediaClass.IMAGE, title="albums", @@ -130,13 +130,13 @@ class ImmichMediaSource(MediaSource): return [ BrowseMediaSource( domain=DOMAIN, - identifier=f"{identifier.unique_id}/albums/{album.album_id}", + identifier=f"{identifier.unique_id}|albums|{album.album_id}", media_class=MediaClass.DIRECTORY, media_content_type=MediaClass.IMAGE, title=album.name, can_play=False, can_expand=True, - thumbnail=f"/immich/{identifier.unique_id}/{album.thumbnail_asset_id}/thumb.jpg/thumbnail", + thumbnail=f"/immich/{identifier.unique_id}/{album.thumbnail_asset_id}/thumbnail/image/jpg", ) for album in albums ] @@ -157,17 +157,18 @@ class ImmichMediaSource(MediaSource): BrowseMediaSource( domain=DOMAIN, identifier=( - f"{identifier.unique_id}/albums/" - f"{identifier.collection_id}/" - f"{asset.asset_id}/" - f"{asset.file_name}" + f"{identifier.unique_id}|albums|" + f"{identifier.collection_id}|" + f"{asset.asset_id}|" + f"{asset.file_name}|" + f"{asset.mime_type}" ), media_class=MediaClass.IMAGE, media_content_type=asset.mime_type, title=asset.file_name, can_play=False, can_expand=False, - thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/{asset.file_name}/thumbnail", + thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/thumbnail/{asset.mime_type}", ) for asset in album_info.assets if asset.mime_type.startswith("image/") @@ -177,17 +178,18 @@ class ImmichMediaSource(MediaSource): BrowseMediaSource( domain=DOMAIN, identifier=( - f"{identifier.unique_id}/albums/" - f"{identifier.collection_id}/" - f"{asset.asset_id}/" - f"{asset.file_name}" + f"{identifier.unique_id}|albums|" + f"{identifier.collection_id}|" + f"{asset.asset_id}|" + f"{asset.file_name}|" + f"{asset.mime_type}" ), media_class=MediaClass.VIDEO, media_content_type=asset.mime_type, title=asset.file_name, can_play=True, can_expand=False, - thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/thumbnail.jpg/thumbnail", + thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/thumbnail/image/jpeg", ) for asset in album_info.assets if asset.mime_type.startswith("video/") @@ -197,17 +199,23 @@ class ImmichMediaSource(MediaSource): async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: """Resolve media to a url.""" - identifier = ImmichMediaSourceIdentifier(item.identifier) - if identifier.file_name is None: - raise Unresolvable("No file name") - mime_type, _ = mimetypes.guess_type(identifier.file_name) - if not isinstance(mime_type, str): - raise Unresolvable("No file extension") + try: + identifier = ImmichMediaSourceIdentifier(item.identifier) + except IndexError as err: + raise Unresolvable( + f"Could not parse identifier: {item.identifier}" + ) from err + + if identifier.mime_type is None: + raise Unresolvable( + f"Could not resolve identifier that has no mime-type: {item.identifier}" + ) + return PlayMedia( ( - f"/immich/{identifier.unique_id}/{identifier.asset_id}/{identifier.file_name}/fullsize" + f"/immich/{identifier.unique_id}/{identifier.asset_id}/fullsize/{identifier.mime_type}" ), - mime_type, + identifier.mime_type, ) @@ -228,10 +236,10 @@ class ImmichMediaView(HomeAssistantView): if not self.hass.config_entries.async_loaded_entries(DOMAIN): raise HTTPNotFound - asset_id, file_name, size = location.split("/") - mime_type, _ = mimetypes.guess_type(file_name) - if not isinstance(mime_type, str): - raise HTTPNotFound + try: + asset_id, size, mime_type_base, mime_type_format = location.split("/") + except ValueError as err: + raise HTTPNotFound from err entry: ImmichConfigEntry | None = ( self.hass.config_entries.async_entry_for_domain_unique_id( @@ -242,7 +250,7 @@ class ImmichMediaView(HomeAssistantView): immich_api = entry.runtime_data.api # stream response for videos - if mime_type.startswith("video/"): + if mime_type_base == "video": try: resp = await immich_api.assets.async_play_video_stream(asset_id) except ImmichError as exc: @@ -259,4 +267,4 @@ class ImmichMediaView(HomeAssistantView): image = await immich_api.assets.async_view_asset(asset_id, size) except ImmichError as exc: raise HTTPNotFound from exc - return Response(body=image, content_type=mime_type) + return Response(body=image, content_type=f"{mime_type_base}/{mime_type_format}") diff --git a/tests/components/immich/test_media_source.py b/tests/components/immich/test_media_source.py index 0f448fbf23d..5b396a780cc 100644 --- a/tests/components/immich/test_media_source.py +++ b/tests/components/immich/test_media_source.py @@ -43,9 +43,15 @@ async def test_get_media_source(hass: HomeAssistant) -> None: @pytest.mark.parametrize( ("identifier", "exception_msg"), [ - ("unique_id", "No file name"), - ("unique_id/albums/album_id", "No file name"), - ("unique_id/albums/album_id/asset_id/filename", "No file extension"), + ("unique_id", "Could not resolve identifier that has no mime-type"), + ( + "unique_id|albums|album_id", + "Could not resolve identifier that has no mime-type", + ), + ( + "unique_id|albums|album_id|asset_id|filename", + "Could not parse identifier", + ), ], ) async def test_resolve_media_bad_identifier( @@ -64,15 +70,20 @@ async def test_resolve_media_bad_identifier( ("identifier", "url", "mime_type"), [ ( - "unique_id/albums/album_id/asset_id/filename.jpg", - "/immich/unique_id/asset_id/filename.jpg/fullsize", + "unique_id|albums|album_id|asset_id|filename.jpg|image/jpeg", + "/immich/unique_id/asset_id/fullsize/image/jpeg", "image/jpeg", ), ( - "unique_id/albums/album_id/asset_id/filename.png", - "/immich/unique_id/asset_id/filename.png/fullsize", + "unique_id|albums|album_id|asset_id|filename.png|image/png", + "/immich/unique_id/asset_id/fullsize/image/png", "image/png", ), + ( + "unique_id|albums|album_id|asset_id|filename.mp4|video/mp4", + "/immich/unique_id/asset_id/fullsize/video/mp4", + "video/mp4", + ), ], ) async def test_resolve_media_success( @@ -137,7 +148,7 @@ async def test_browse_media_get_root( assert isinstance(media_file, BrowseMedia) assert media_file.title == "albums" assert media_file.media_content_id == ( - "media-source://immich/e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums" + "media-source://immich/e7ef5713-9dab-4bd4-b899-715b0ca4379e|albums" ) @@ -154,7 +165,7 @@ async def test_browse_media_get_albums( source = await async_get_media_source(hass) item = MediaSourceItem( - hass, DOMAIN, "e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums", None + hass, DOMAIN, "e7ef5713-9dab-4bd4-b899-715b0ca4379e|albums", None ) result = await source.async_browse_media(item) @@ -165,7 +176,7 @@ async def test_browse_media_get_albums( assert media_file.title == "My Album" assert media_file.media_content_id == ( "media-source://immich/" - "e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums/" + "e7ef5713-9dab-4bd4-b899-715b0ca4379e|albums|" "721e1a4b-aa12-441e-8d3b-5ac7ab283bb6" ) @@ -193,7 +204,7 @@ async def test_browse_media_get_albums_error( source = await async_get_media_source(hass) - item = MediaSourceItem(hass, DOMAIN, f"{mock_config_entry.unique_id}/albums", None) + item = MediaSourceItem(hass, DOMAIN, f"{mock_config_entry.unique_id}|albums", None) result = await source.async_browse_media(item) assert result @@ -219,7 +230,7 @@ async def test_browse_media_get_album_items_error( item = MediaSourceItem( hass, DOMAIN, - "e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums/721e1a4b-aa12-441e-8d3b-5ac7ab283bb6", + "e7ef5713-9dab-4bd4-b899-715b0ca4379e|albums|721e1a4b-aa12-441e-8d3b-5ac7ab283bb6", None, ) result = await source.async_browse_media(item) @@ -240,7 +251,7 @@ async def test_browse_media_get_album_items_error( item = MediaSourceItem( hass, DOMAIN, - "e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums/721e1a4b-aa12-441e-8d3b-5ac7ab283bb6", + "e7ef5713-9dab-4bd4-b899-715b0ca4379e|albums|721e1a4b-aa12-441e-8d3b-5ac7ab283bb6", None, ) result = await source.async_browse_media(item) @@ -266,7 +277,7 @@ async def test_browse_media_get_album_items( item = MediaSourceItem( hass, DOMAIN, - "e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums/721e1a4b-aa12-441e-8d3b-5ac7ab283bb6", + "e7ef5713-9dab-4bd4-b899-715b0ca4379e|albums|721e1a4b-aa12-441e-8d3b-5ac7ab283bb6", None, ) result = await source.async_browse_media(item) @@ -276,9 +287,9 @@ async def test_browse_media_get_album_items( media_file = result.children[0] assert isinstance(media_file, BrowseMedia) assert media_file.identifier == ( - "e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums/" - "721e1a4b-aa12-441e-8d3b-5ac7ab283bb6/" - "2e94c203-50aa-4ad2-8e29-56dd74e0eff4/filename.jpg" + "e7ef5713-9dab-4bd4-b899-715b0ca4379e|albums|" + "721e1a4b-aa12-441e-8d3b-5ac7ab283bb6|" + "2e94c203-50aa-4ad2-8e29-56dd74e0eff4|filename.jpg|image/jpeg" ) assert media_file.title == "filename.jpg" assert media_file.media_class == MediaClass.IMAGE @@ -287,15 +298,15 @@ async def test_browse_media_get_album_items( assert not media_file.can_expand assert media_file.thumbnail == ( "/immich/e7ef5713-9dab-4bd4-b899-715b0ca4379e/" - "2e94c203-50aa-4ad2-8e29-56dd74e0eff4/filename.jpg/thumbnail" + "2e94c203-50aa-4ad2-8e29-56dd74e0eff4/thumbnail/image/jpeg" ) media_file = result.children[1] assert isinstance(media_file, BrowseMedia) assert media_file.identifier == ( - "e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums/" - "721e1a4b-aa12-441e-8d3b-5ac7ab283bb6/" - "2e65a5f2-db83-44c4-81ab-f5ff20c9bd7b/filename.mp4" + "e7ef5713-9dab-4bd4-b899-715b0ca4379e|albums|" + "721e1a4b-aa12-441e-8d3b-5ac7ab283bb6|" + "2e65a5f2-db83-44c4-81ab-f5ff20c9bd7b|filename.mp4|video/mp4" ) assert media_file.title == "filename.mp4" assert media_file.media_class == MediaClass.VIDEO @@ -304,7 +315,7 @@ async def test_browse_media_get_album_items( assert not media_file.can_expand assert media_file.thumbnail == ( "/immich/e7ef5713-9dab-4bd4-b899-715b0ca4379e/" - "2e65a5f2-db83-44c4-81ab-f5ff20c9bd7b/thumbnail.jpg/thumbnail" + "2e65a5f2-db83-44c4-81ab-f5ff20c9bd7b/thumbnail/image/jpeg" ) @@ -327,12 +338,12 @@ async def test_media_view( with patch("homeassistant.components.immich.PLATFORMS", []): await setup_integration(hass, mock_config_entry) - # wrong url (without file extension) + # wrong url (without mime type) with pytest.raises(web.HTTPNotFound): await view.get( request, "e7ef5713-9dab-4bd4-b899-715b0ca4379e", - "2e94c203-50aa-4ad2-8e29-56dd74e0eff4/filename/thumbnail", + "2e94c203-50aa-4ad2-8e29-56dd74e0eff4/thumbnail", ) # exception in async_view_asset() @@ -348,7 +359,7 @@ async def test_media_view( await view.get( request, "e7ef5713-9dab-4bd4-b899-715b0ca4379e", - "2e94c203-50aa-4ad2-8e29-56dd74e0eff4/filename.jpg/thumbnail", + "2e94c203-50aa-4ad2-8e29-56dd74e0eff4/thumbnail/image/jpeg", ) # exception in async_play_video_stream() @@ -364,7 +375,7 @@ async def test_media_view( await view.get( request, "e7ef5713-9dab-4bd4-b899-715b0ca4379e", - "2e65a5f2-db83-44c4-81ab-f5ff20c9bd7b/filename.mp4/fullsize", + "2e65a5f2-db83-44c4-81ab-f5ff20c9bd7b/fullsize/video/mp4", ) # success @@ -374,14 +385,14 @@ async def test_media_view( result = await view.get( request, "e7ef5713-9dab-4bd4-b899-715b0ca4379e", - "2e94c203-50aa-4ad2-8e29-56dd74e0eff4/filename.jpg/thumbnail", + "2e94c203-50aa-4ad2-8e29-56dd74e0eff4/thumbnail/image/jpeg", ) assert isinstance(result, web.Response) with patch.object(tempfile, "tempdir", tmp_path): result = await view.get( request, "e7ef5713-9dab-4bd4-b899-715b0ca4379e", - "2e94c203-50aa-4ad2-8e29-56dd74e0eff4/filename.jpg/fullsize", + "2e94c203-50aa-4ad2-8e29-56dd74e0eff4/fullsize/image/jpeg", ) assert isinstance(result, web.Response) @@ -393,6 +404,6 @@ async def test_media_view( result = await view.get( request, "e7ef5713-9dab-4bd4-b899-715b0ca4379e", - "2e65a5f2-db83-44c4-81ab-f5ff20c9bd7b/filename.mp4/fullsize", + "2e65a5f2-db83-44c4-81ab-f5ff20c9bd7b/fullsize/video/mp4", ) assert isinstance(result, web.StreamResponse) From 64b4642c496caf1043a7d24808429242994d8730 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Thu, 29 May 2025 11:58:29 +1000 Subject: [PATCH 050/266] Fix Tessie volume max and step (#145835) * Use fixed volume max and step * Update snapshot --- homeassistant/components/tessie/media_player.py | 9 ++++++--- tests/components/tessie/snapshots/test_media_player.ambr | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/tessie/media_player.py b/homeassistant/components/tessie/media_player.py index 139ee07ca5b..ecac11587c1 100644 --- a/homeassistant/components/tessie/media_player.py +++ b/homeassistant/components/tessie/media_player.py @@ -20,6 +20,10 @@ STATES = { "Stopped": MediaPlayerState.IDLE, } +# Tesla uses 31 steps, in 0.333 increments up to 10.333 +VOLUME_STEP = 1 / 31 +VOLUME_FACTOR = 31 / 3 # 10.333 + PARALLEL_UPDATES = 0 @@ -38,6 +42,7 @@ class TessieMediaEntity(TessieEntity, MediaPlayerEntity): """Vehicle Location Media Class.""" _attr_device_class = MediaPlayerDeviceClass.SPEAKER + _attr_volume_step = VOLUME_STEP def __init__( self, @@ -57,9 +62,7 @@ class TessieMediaEntity(TessieEntity, MediaPlayerEntity): @property def volume_level(self) -> float: """Volume level of the media player (0..1).""" - return self.get("vehicle_state_media_info_audio_volume", 0) / self.get( - "vehicle_state_media_info_audio_volume_max", 10.333333 - ) + return self.get("vehicle_state_media_info_audio_volume", 0) / VOLUME_FACTOR @property def media_duration(self) -> int | None: diff --git a/tests/components/tessie/snapshots/test_media_player.ambr b/tests/components/tessie/snapshots/test_media_player.ambr index ff0f6c794a7..69a5ca4b86b 100644 --- a/tests/components/tessie/snapshots/test_media_player.ambr +++ b/tests/components/tessie/snapshots/test_media_player.ambr @@ -41,7 +41,7 @@ 'device_class': 'speaker', 'friendly_name': 'Test Media player', 'supported_features': , - 'volume_level': 0.22580323309042688, + 'volume_level': 0.2258032258064516, }), 'context': , 'entity_id': 'media_player.test_media_player', @@ -64,7 +64,7 @@ 'media_title': 'Song', 'source': 'Spotify', 'supported_features': , - 'volume_level': 0.22580323309042688, + 'volume_level': 0.2258032258064516, }), 'context': , 'entity_id': 'media_player.test_media_player', From 097eecd78a28b3f2183de940d2155edfc930c965 Mon Sep 17 00:00:00 2001 From: Matthew FitzGerald-Chamberlain Date: Wed, 28 May 2025 20:43:28 -0500 Subject: [PATCH 051/266] Bump pyaprilaire to 0.9.1 (#145836) --- homeassistant/components/aprilaire/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aprilaire/manifest.json b/homeassistant/components/aprilaire/manifest.json index 6fe3beae3bc..fa30882f669 100644 --- a/homeassistant/components/aprilaire/manifest.json +++ b/homeassistant/components/aprilaire/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "local_push", "loggers": ["pyaprilaire"], - "requirements": ["pyaprilaire==0.9.0"] + "requirements": ["pyaprilaire==0.9.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 0aad41a4bcc..8a9ce0e353a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1832,7 +1832,7 @@ pyairnow==1.2.1 pyairvisual==2023.08.1 # homeassistant.components.aprilaire -pyaprilaire==0.9.0 +pyaprilaire==0.9.1 # homeassistant.components.asuswrt pyasuswrt==0.1.21 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d358800b7f4..142ba2e57e9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1531,7 +1531,7 @@ pyairnow==1.2.1 pyairvisual==2023.08.1 # homeassistant.components.aprilaire -pyaprilaire==0.9.0 +pyaprilaire==0.9.1 # homeassistant.components.asuswrt pyasuswrt==0.1.21 From 5cfccb7e1d7dddccc41619bb5cc10462f65ffaea Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 28 May 2025 19:38:06 -0500 Subject: [PATCH 052/266] Bump aiohttp to 3.12.3 (#145837) --- 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 33766ec52c4..89c2d39edd8 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -6,7 +6,7 @@ aiodns==3.4.0 aiohasupervisor==0.3.1 aiohttp-asyncmdnsresolver==0.1.1 aiohttp-fast-zlib==0.2.3 -aiohttp==3.12.2 +aiohttp==3.12.3 aiohttp_cors==0.7.0 aiousbwatcher==1.1.1 aiozoneinfo==0.2.3 diff --git a/pyproject.toml b/pyproject.toml index 04dcb3e3057..4a7022b7b67 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.2", + "aiohttp==3.12.3", "aiohttp_cors==0.7.0", "aiohttp-fast-zlib==0.2.3", "aiohttp-asyncmdnsresolver==0.1.1", diff --git a/requirements.txt b/requirements.txt index 403948d1445..bc044454da4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ # Home Assistant Core aiodns==3.4.0 aiohasupervisor==0.3.1 -aiohttp==3.12.2 +aiohttp==3.12.3 aiohttp_cors==0.7.0 aiohttp-fast-zlib==0.2.3 aiohttp-asyncmdnsresolver==0.1.1 From 4317fad79858f23c89c8ef2d74663d4e8936527a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 28 May 2025 21:08:01 -0500 Subject: [PATCH 053/266] Bump aiohttp to 3.12.4 (#145838) --- 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 89c2d39edd8..8dde4a6a654 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -6,7 +6,7 @@ aiodns==3.4.0 aiohasupervisor==0.3.1 aiohttp-asyncmdnsresolver==0.1.1 aiohttp-fast-zlib==0.2.3 -aiohttp==3.12.3 +aiohttp==3.12.4 aiohttp_cors==0.7.0 aiousbwatcher==1.1.1 aiozoneinfo==0.2.3 diff --git a/pyproject.toml b/pyproject.toml index 4a7022b7b67..331381def64 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.3", + "aiohttp==3.12.4", "aiohttp_cors==0.7.0", "aiohttp-fast-zlib==0.2.3", "aiohttp-asyncmdnsresolver==0.1.1", diff --git a/requirements.txt b/requirements.txt index bc044454da4..f1ced51637e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ # Home Assistant Core aiodns==3.4.0 aiohasupervisor==0.3.1 -aiohttp==3.12.3 +aiohttp==3.12.4 aiohttp_cors==0.7.0 aiohttp-fast-zlib==0.2.3 aiohttp-asyncmdnsresolver==0.1.1 From 0f7379c941feba20e4930b4e1d838fff3d6dfeb3 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 29 May 2025 15:31:50 +0200 Subject: [PATCH 054/266] Reolink fallback to download command for playback (#145842) --- homeassistant/components/reolink/views.py | 27 ++++++++++++++++++++++- tests/components/reolink/test_views.py | 7 +++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/reolink/views.py b/homeassistant/components/reolink/views.py index 44265244b18..7f062055f7e 100644 --- a/homeassistant/components/reolink/views.py +++ b/homeassistant/components/reolink/views.py @@ -52,6 +52,7 @@ class PlaybackProxyView(HomeAssistantView): verify_ssl=False, ssl_cipher=SSLCipherList.INSECURE, ) + self._vod_type: str | None = None async def get( self, @@ -68,6 +69,8 @@ class PlaybackProxyView(HomeAssistantView): filename_decoded = urlsafe_b64decode(filename.encode("utf-8")).decode("utf-8") ch = int(channel) + if self._vod_type is not None: + vod_type = self._vod_type try: host = get_host(self.hass, config_entry_id) except Unresolvable: @@ -127,6 +130,25 @@ class PlaybackProxyView(HomeAssistantView): "apolication/octet-stream", ]: err_str = f"Reolink playback expected video/mp4 but got {reolink_response.content_type}" + if ( + reolink_response.content_type == "video/x-flv" + and vod_type == VodRequestType.PLAYBACK.value + ): + # next time use DOWNLOAD immediately + self._vod_type = VodRequestType.DOWNLOAD.value + _LOGGER.debug( + "%s, retrying using download instead of playback cmd", err_str + ) + return await self.get( + request, + config_entry_id, + channel, + stream_res, + self._vod_type, + filename, + retry, + ) + _LOGGER.error(err_str) if reolink_response.content_type == "text/html": text = await reolink_response.text() @@ -140,7 +162,10 @@ class PlaybackProxyView(HomeAssistantView): reolink_response.reason, response_headers, ) - response_headers["Content-Type"] = "video/mp4" + if "Content-Type" not in response_headers: + response_headers["Content-Type"] = reolink_response.content_type + if response_headers["Content-Type"] == "apolication/octet-stream": + response_headers["Content-Type"] = "application/octet-stream" response = web.StreamResponse( status=reolink_response.status, diff --git a/tests/components/reolink/test_views.py b/tests/components/reolink/test_views.py index 3521de072b6..992e47f0575 100644 --- a/tests/components/reolink/test_views.py +++ b/tests/components/reolink/test_views.py @@ -58,17 +58,22 @@ def get_mock_session( return mock_session +@pytest.mark.parametrize( + ("content_type"), + [("video/mp4"), ("application/octet-stream"), ("apolication/octet-stream")], +) async def test_playback_proxy( hass: HomeAssistant, reolink_connect: MagicMock, config_entry: MockConfigEntry, hass_client: ClientSessionGenerator, caplog: pytest.LogCaptureFixture, + content_type: str, ) -> None: """Test successful playback proxy URL.""" reolink_connect.get_vod_source.return_value = (TEST_MIME_TYPE_MP4, TEST_URL) - mock_session = get_mock_session() + mock_session = get_mock_session(content_type=content_type) with patch( "homeassistant.components.reolink.views.async_get_clientsession", From d46f28792c3f1739d87568a77bb220131b2c1800 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Thu, 29 May 2025 15:30:02 +0200 Subject: [PATCH 055/266] Bump aioimmich to 0.7.0 (#145845) --- homeassistant/components/immich/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/immich/manifest.json b/homeassistant/components/immich/manifest.json index 454adae5501..5b56a7e3e2d 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.6.0"] + "requirements": ["aioimmich==0.7.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 8a9ce0e353a..6aeaf8b077f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -280,7 +280,7 @@ aiohue==4.7.4 aioimaplib==2.0.1 # homeassistant.components.immich -aioimmich==0.6.0 +aioimmich==0.7.0 # homeassistant.components.apache_kafka aiokafka==0.10.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 142ba2e57e9..ae0b8ee7cd2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -265,7 +265,7 @@ aiohue==4.7.4 aioimaplib==2.0.1 # homeassistant.components.immich -aioimmich==0.6.0 +aioimmich==0.7.0 # homeassistant.components.apache_kafka aiokafka==0.10.0 From 600ac17a5f459e922b0d89c41aee4a4f5f672c81 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Thu, 29 May 2025 14:12:51 +0200 Subject: [PATCH 056/266] Deprecate sms integration (#145847) --- .../components/homeassistant/strings.json | 4 ++ homeassistant/components/sms/__init__.py | 24 +++++++- tests/components/sms/test_init.py | 59 +++++++++++++++++++ 3 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 tests/components/sms/test_init.py diff --git a/homeassistant/components/homeassistant/strings.json b/homeassistant/components/homeassistant/strings.json index e4c3e19cf7c..123e625d0fc 100644 --- a/homeassistant/components/homeassistant/strings.json +++ b/homeassistant/components/homeassistant/strings.json @@ -18,6 +18,10 @@ "title": "The {integration_title} YAML configuration is being removed", "description": "Configuring {integration_title} using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue." }, + "deprecated_system_packages_config_flow_integration": { + "title": "The {integration_title} integration is being removed", + "description": "The {integration_title} integration is being removed as it requires additional system packages, which can't be installed on supported Home Assistant installations. Remove all \"{integration_title}\" config entries to fix this issue." + }, "deprecated_system_packages_yaml_integration": { "title": "The {integration_title} integration is being removed", "description": "The {integration_title} integration is being removed as it requires additional system packages, which can't be installed on supported Home Assistant installations. Remove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue." diff --git a/homeassistant/components/sms/__init__.py b/homeassistant/components/sms/__init__.py index 2d18d44de3a..6c7c5374f7d 100644 --- a/homeassistant/components/sms/__init__.py +++ b/homeassistant/components/sms/__init__.py @@ -6,9 +6,14 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE, CONF_NAME, Platform -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, discovery +from homeassistant.helpers.issue_registry import ( + IssueSeverity, + async_create_issue, + async_delete_issue, +) from homeassistant.helpers.typing import ConfigType from .const import ( @@ -41,6 +46,7 @@ CONFIG_SCHEMA = vol.Schema( }, extra=vol.ALLOW_EXTRA, ) +DEPRECATED_ISSUE_ID = f"deprecated_system_packages_config_flow_integration_{DOMAIN}" async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -52,6 +58,19 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Configure Gammu state machine.""" + async_create_issue( + hass, + HOMEASSISTANT_DOMAIN, + DEPRECATED_ISSUE_ID, + breaks_in_ha_version="2025.12.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_system_packages_config_flow_integration", + translation_placeholders={ + "integration_title": "SMS notifications via GSM-modem", + }, + ) device = entry.data[CONF_DEVICE] connection_mode = "at" @@ -101,4 +120,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: gateway = hass.data[DOMAIN].pop(SMS_GATEWAY)[GATEWAY] await gateway.terminate_async() + if not hass.config_entries.async_loaded_entries(DOMAIN): + async_delete_issue(hass, HOMEASSISTANT_DOMAIN, DEPRECATED_ISSUE_ID) + return unload_ok diff --git a/tests/components/sms/test_init.py b/tests/components/sms/test_init.py new file mode 100644 index 00000000000..03cebfe9b52 --- /dev/null +++ b/tests/components/sms/test_init.py @@ -0,0 +1,59 @@ +"""Test init.""" + +from unittest.mock import Mock, patch + +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_DEVICE +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.helpers import issue_registry as ir + +from tests.common import MockConfigEntry + + +@patch.dict( + "sys.modules", + { + "gammu": Mock(), + "gammu.asyncworker": Mock(), + }, +) +async def test_repair_issue_is_created( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, +) -> None: + """Test repair issue is created.""" + from homeassistant.components.sms import ( # pylint: disable=import-outside-toplevel + DEPRECATED_ISSUE_ID, + DOMAIN as SMS_DOMAIN, + ) + + with ( + patch("homeassistant.components.sms.create_sms_gateway", autospec=True), + patch("homeassistant.components.sms.PLATFORMS", []), + ): + config_entry = MockConfigEntry( + title="test", + domain=SMS_DOMAIN, + data={ + CONF_DEVICE: "/dev/ttyUSB0", + }, + ) + + 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 + assert ( + HOMEASSISTANT_DOMAIN, + DEPRECATED_ISSUE_ID, + ) in issue_registry.issues + + await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.NOT_LOADED + assert ( + HOMEASSISTANT_DOMAIN, + DEPRECATED_ISSUE_ID, + ) not in issue_registry.issues From 48103bd2446df6793377a07332f28e9efea644ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Diego=20Rodr=C3=ADguez=20Royo?= Date: Fri, 30 May 2025 01:40:11 +0200 Subject: [PATCH 057/266] Bump aiohomeconnect to 0.17.1 (#145873) --- homeassistant/components/home_connect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/home_connect/manifest.json b/homeassistant/components/home_connect/manifest.json index e8a36cd60d9..d4b37552fb7 100644 --- a/homeassistant/components/home_connect/manifest.json +++ b/homeassistant/components/home_connect/manifest.json @@ -21,6 +21,6 @@ "documentation": "https://www.home-assistant.io/integrations/home_connect", "iot_class": "cloud_push", "loggers": ["aiohomeconnect"], - "requirements": ["aiohomeconnect==0.17.0"], + "requirements": ["aiohomeconnect==0.17.1"], "zeroconf": ["_homeconnect._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 6aeaf8b077f..68ec08b1c90 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -265,7 +265,7 @@ aioharmony==0.5.2 aiohasupervisor==0.3.1 # homeassistant.components.home_connect -aiohomeconnect==0.17.0 +aiohomeconnect==0.17.1 # homeassistant.components.homekit_controller aiohomekit==3.2.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ae0b8ee7cd2..77661e0f624 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -250,7 +250,7 @@ aioharmony==0.5.2 aiohasupervisor==0.3.1 # homeassistant.components.home_connect -aiohomeconnect==0.17.0 +aiohomeconnect==0.17.1 # homeassistant.components.homekit_controller aiohomekit==3.2.14 From aa8a6058b5b2d5edf758fa2ebab508cedb1934b6 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 30 May 2025 12:56:51 +0200 Subject: [PATCH 058/266] Bump version to 2025.6.0b3 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5a6ac701771..29095678e48 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 = 6 -PATCH_VERSION: Final = "0b2" +PATCH_VERSION: Final = "0b3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index 331381def64..dc185344ca8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.6.0b2" +version = "2025.6.0b3" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From f0fcef574432f45a971785dce9f3c9cecd9d9ea3 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 31 May 2025 20:25:24 +0200 Subject: [PATCH 059/266] Add more Amazon Devices DHCP matches (#145776) --- .../components/amazon_devices/manifest.json | 89 ++++- homeassistant/generated/dhcp.py | 348 ++++++++++++++++++ 2 files changed, 436 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/amazon_devices/manifest.json b/homeassistant/components/amazon_devices/manifest.json index 7593fbd4943..eb9fae6ddbe 100644 --- a/homeassistant/components/amazon_devices/manifest.json +++ b/homeassistant/components/amazon_devices/manifest.json @@ -4,27 +4,114 @@ "codeowners": ["@chemelli74"], "config_flow": true, "dhcp": [ + { "macaddress": "007147*" }, + { "macaddress": "00FC8B*" }, + { "macaddress": "0812A5*" }, + { "macaddress": "086AE5*" }, + { "macaddress": "08849D*" }, + { "macaddress": "089115*" }, { "macaddress": "08A6BC*" }, + { "macaddress": "08C224*" }, + { "macaddress": "0CDC91*" }, + { "macaddress": "0CEE99*" }, + { "macaddress": "1009F9*" }, + { "macaddress": "109693*" }, { "macaddress": "10BF67*" }, + { "macaddress": "10CE02*" }, + { "macaddress": "140AC5*" }, + { "macaddress": "149138*" }, + { "macaddress": "1848BE*" }, + { "macaddress": "1C12B0*" }, + { "macaddress": "1C4D66*" }, + { "macaddress": "1C93C4*" }, + { "macaddress": "1CFE2B*" }, + { "macaddress": "244CE3*" }, + { "macaddress": "24CE33*" }, + { "macaddress": "2873F6*" }, + { "macaddress": "2C71FF*" }, + { "macaddress": "34AFB3*" }, + { "macaddress": "34D270*" }, + { "macaddress": "38F73D*" }, + { "macaddress": "3C5CC4*" }, + { "macaddress": "3CE441*" }, { "macaddress": "440049*" }, + { "macaddress": "40A2DB*" }, + { "macaddress": "40A9CF*" }, + { "macaddress": "40B4CD*" }, { "macaddress": "443D54*" }, + { "macaddress": "44650D*" }, + { "macaddress": "485F2D*" }, + { "macaddress": "48785E*" }, { "macaddress": "48B423*" }, { "macaddress": "4C1744*" }, + { "macaddress": "4CEFC0*" }, + { "macaddress": "5007C3*" }, { "macaddress": "50D45C*" }, { "macaddress": "50DCE7*" }, + { "macaddress": "50F5DA*" }, + { "macaddress": "5C415A*" }, + { "macaddress": "6837E9*" }, + { "macaddress": "6854FD*" }, + { "macaddress": "689A87*" }, + { "macaddress": "68B691*" }, + { "macaddress": "68DBF5*" }, { "macaddress": "68F63B*" }, { "macaddress": "6C0C9A*" }, + { "macaddress": "6C5697*" }, + { "macaddress": "7458F3*" }, + { "macaddress": "74C246*" }, { "macaddress": "74D637*" }, + { "macaddress": "74E20C*" }, + { "macaddress": "74ECB2*" }, + { "macaddress": "786C84*" }, + { "macaddress": "78A03F*" }, { "macaddress": "7C6166*" }, + { "macaddress": "7C6305*" }, + { "macaddress": "7CD566*" }, + { "macaddress": "8871E5*" }, { "macaddress": "901195*" }, + { "macaddress": "90235B*" }, + { "macaddress": "90A822*" }, + { "macaddress": "90F82E*" }, { "macaddress": "943A91*" }, { "macaddress": "98226E*" }, + { "macaddress": "98CCF3*" }, { "macaddress": "9CC8E9*" }, + { "macaddress": "A002DC*" }, + { "macaddress": "A0D2B1*" }, + { "macaddress": "A40801*" }, { "macaddress": "A8E621*" }, + { "macaddress": "AC416A*" }, + { "macaddress": "AC63BE*" }, + { "macaddress": "ACCCFC*" }, + { "macaddress": "B0739C*" }, + { "macaddress": "B0CFCB*" }, + { "macaddress": "B0F7C4*" }, + { "macaddress": "B85F98*" }, + { "macaddress": "C091B9*" }, { "macaddress": "C095CF*" }, + { "macaddress": "C49500*" }, + { "macaddress": "C86C3D*" }, + { "macaddress": "CC9EA2*" }, + { "macaddress": "CCF735*" }, + { "macaddress": "DC54D7*" }, { "macaddress": "D8BE65*" }, + { "macaddress": "D8FBD6*" }, + { "macaddress": "DC91BF*" }, + { "macaddress": "DCA0D0*" }, + { "macaddress": "E0F728*" }, { "macaddress": "EC2BEB*" }, - { "macaddress": "F02F9E*" } + { "macaddress": "EC8AC4*" }, + { "macaddress": "ECA138*" }, + { "macaddress": "F02F9E*" }, + { "macaddress": "F0272D*" }, + { "macaddress": "F0F0A4*" }, + { "macaddress": "F4032A*" }, + { "macaddress": "F854B8*" }, + { "macaddress": "FC492D*" }, + { "macaddress": "FC65DE*" }, + { "macaddress": "FCA183*" }, + { "macaddress": "FCE9D8*" } ], "documentation": "https://www.home-assistant.io/integrations/amazon_devices", "integration_type": "hub", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 0fb8b48b01c..5285ab7a1db 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -26,22 +26,158 @@ DHCP: Final[list[dict[str, str | bool]]] = [ "domain": "airzone", "macaddress": "E84F25*", }, + { + "domain": "amazon_devices", + "macaddress": "007147*", + }, + { + "domain": "amazon_devices", + "macaddress": "00FC8B*", + }, + { + "domain": "amazon_devices", + "macaddress": "0812A5*", + }, + { + "domain": "amazon_devices", + "macaddress": "086AE5*", + }, + { + "domain": "amazon_devices", + "macaddress": "08849D*", + }, + { + "domain": "amazon_devices", + "macaddress": "089115*", + }, { "domain": "amazon_devices", "macaddress": "08A6BC*", }, + { + "domain": "amazon_devices", + "macaddress": "08C224*", + }, + { + "domain": "amazon_devices", + "macaddress": "0CDC91*", + }, + { + "domain": "amazon_devices", + "macaddress": "0CEE99*", + }, + { + "domain": "amazon_devices", + "macaddress": "1009F9*", + }, + { + "domain": "amazon_devices", + "macaddress": "109693*", + }, { "domain": "amazon_devices", "macaddress": "10BF67*", }, + { + "domain": "amazon_devices", + "macaddress": "10CE02*", + }, + { + "domain": "amazon_devices", + "macaddress": "140AC5*", + }, + { + "domain": "amazon_devices", + "macaddress": "149138*", + }, + { + "domain": "amazon_devices", + "macaddress": "1848BE*", + }, + { + "domain": "amazon_devices", + "macaddress": "1C12B0*", + }, + { + "domain": "amazon_devices", + "macaddress": "1C4D66*", + }, + { + "domain": "amazon_devices", + "macaddress": "1C93C4*", + }, + { + "domain": "amazon_devices", + "macaddress": "1CFE2B*", + }, + { + "domain": "amazon_devices", + "macaddress": "244CE3*", + }, + { + "domain": "amazon_devices", + "macaddress": "24CE33*", + }, + { + "domain": "amazon_devices", + "macaddress": "2873F6*", + }, + { + "domain": "amazon_devices", + "macaddress": "2C71FF*", + }, + { + "domain": "amazon_devices", + "macaddress": "34AFB3*", + }, + { + "domain": "amazon_devices", + "macaddress": "34D270*", + }, + { + "domain": "amazon_devices", + "macaddress": "38F73D*", + }, + { + "domain": "amazon_devices", + "macaddress": "3C5CC4*", + }, + { + "domain": "amazon_devices", + "macaddress": "3CE441*", + }, { "domain": "amazon_devices", "macaddress": "440049*", }, + { + "domain": "amazon_devices", + "macaddress": "40A2DB*", + }, + { + "domain": "amazon_devices", + "macaddress": "40A9CF*", + }, + { + "domain": "amazon_devices", + "macaddress": "40B4CD*", + }, { "domain": "amazon_devices", "macaddress": "443D54*", }, + { + "domain": "amazon_devices", + "macaddress": "44650D*", + }, + { + "domain": "amazon_devices", + "macaddress": "485F2D*", + }, + { + "domain": "amazon_devices", + "macaddress": "48785E*", + }, { "domain": "amazon_devices", "macaddress": "48B423*", @@ -50,6 +186,14 @@ DHCP: Final[list[dict[str, str | bool]]] = [ "domain": "amazon_devices", "macaddress": "4C1744*", }, + { + "domain": "amazon_devices", + "macaddress": "4CEFC0*", + }, + { + "domain": "amazon_devices", + "macaddress": "5007C3*", + }, { "domain": "amazon_devices", "macaddress": "50D45C*", @@ -58,6 +202,34 @@ DHCP: Final[list[dict[str, str | bool]]] = [ "domain": "amazon_devices", "macaddress": "50DCE7*", }, + { + "domain": "amazon_devices", + "macaddress": "50F5DA*", + }, + { + "domain": "amazon_devices", + "macaddress": "5C415A*", + }, + { + "domain": "amazon_devices", + "macaddress": "6837E9*", + }, + { + "domain": "amazon_devices", + "macaddress": "6854FD*", + }, + { + "domain": "amazon_devices", + "macaddress": "689A87*", + }, + { + "domain": "amazon_devices", + "macaddress": "68B691*", + }, + { + "domain": "amazon_devices", + "macaddress": "68DBF5*", + }, { "domain": "amazon_devices", "macaddress": "68F63B*", @@ -66,18 +238,70 @@ DHCP: Final[list[dict[str, str | bool]]] = [ "domain": "amazon_devices", "macaddress": "6C0C9A*", }, + { + "domain": "amazon_devices", + "macaddress": "6C5697*", + }, + { + "domain": "amazon_devices", + "macaddress": "7458F3*", + }, + { + "domain": "amazon_devices", + "macaddress": "74C246*", + }, { "domain": "amazon_devices", "macaddress": "74D637*", }, + { + "domain": "amazon_devices", + "macaddress": "74E20C*", + }, + { + "domain": "amazon_devices", + "macaddress": "74ECB2*", + }, + { + "domain": "amazon_devices", + "macaddress": "786C84*", + }, + { + "domain": "amazon_devices", + "macaddress": "78A03F*", + }, { "domain": "amazon_devices", "macaddress": "7C6166*", }, + { + "domain": "amazon_devices", + "macaddress": "7C6305*", + }, + { + "domain": "amazon_devices", + "macaddress": "7CD566*", + }, + { + "domain": "amazon_devices", + "macaddress": "8871E5*", + }, { "domain": "amazon_devices", "macaddress": "901195*", }, + { + "domain": "amazon_devices", + "macaddress": "90235B*", + }, + { + "domain": "amazon_devices", + "macaddress": "90A822*", + }, + { + "domain": "amazon_devices", + "macaddress": "90F82E*", + }, { "domain": "amazon_devices", "macaddress": "943A91*", @@ -86,30 +310,154 @@ DHCP: Final[list[dict[str, str | bool]]] = [ "domain": "amazon_devices", "macaddress": "98226E*", }, + { + "domain": "amazon_devices", + "macaddress": "98CCF3*", + }, { "domain": "amazon_devices", "macaddress": "9CC8E9*", }, + { + "domain": "amazon_devices", + "macaddress": "A002DC*", + }, + { + "domain": "amazon_devices", + "macaddress": "A0D2B1*", + }, + { + "domain": "amazon_devices", + "macaddress": "A40801*", + }, { "domain": "amazon_devices", "macaddress": "A8E621*", }, + { + "domain": "amazon_devices", + "macaddress": "AC416A*", + }, + { + "domain": "amazon_devices", + "macaddress": "AC63BE*", + }, + { + "domain": "amazon_devices", + "macaddress": "ACCCFC*", + }, + { + "domain": "amazon_devices", + "macaddress": "B0739C*", + }, + { + "domain": "amazon_devices", + "macaddress": "B0CFCB*", + }, + { + "domain": "amazon_devices", + "macaddress": "B0F7C4*", + }, + { + "domain": "amazon_devices", + "macaddress": "B85F98*", + }, + { + "domain": "amazon_devices", + "macaddress": "C091B9*", + }, { "domain": "amazon_devices", "macaddress": "C095CF*", }, + { + "domain": "amazon_devices", + "macaddress": "C49500*", + }, + { + "domain": "amazon_devices", + "macaddress": "C86C3D*", + }, + { + "domain": "amazon_devices", + "macaddress": "CC9EA2*", + }, + { + "domain": "amazon_devices", + "macaddress": "CCF735*", + }, + { + "domain": "amazon_devices", + "macaddress": "DC54D7*", + }, { "domain": "amazon_devices", "macaddress": "D8BE65*", }, + { + "domain": "amazon_devices", + "macaddress": "D8FBD6*", + }, + { + "domain": "amazon_devices", + "macaddress": "DC91BF*", + }, + { + "domain": "amazon_devices", + "macaddress": "DCA0D0*", + }, + { + "domain": "amazon_devices", + "macaddress": "E0F728*", + }, { "domain": "amazon_devices", "macaddress": "EC2BEB*", }, + { + "domain": "amazon_devices", + "macaddress": "EC8AC4*", + }, + { + "domain": "amazon_devices", + "macaddress": "ECA138*", + }, { "domain": "amazon_devices", "macaddress": "F02F9E*", }, + { + "domain": "amazon_devices", + "macaddress": "F0272D*", + }, + { + "domain": "amazon_devices", + "macaddress": "F0F0A4*", + }, + { + "domain": "amazon_devices", + "macaddress": "F4032A*", + }, + { + "domain": "amazon_devices", + "macaddress": "F854B8*", + }, + { + "domain": "amazon_devices", + "macaddress": "FC492D*", + }, + { + "domain": "amazon_devices", + "macaddress": "FC65DE*", + }, + { + "domain": "amazon_devices", + "macaddress": "FCA183*", + }, + { + "domain": "amazon_devices", + "macaddress": "FCE9D8*", + }, { "domain": "august", "hostname": "connect", From 9879ecad85565b2fd5d14813d3b522c110c28f01 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Sat, 31 May 2025 20:00:34 +0200 Subject: [PATCH 060/266] Deprecate snips integration (#145784) --- homeassistant/components/snips/__init__.py | 21 ++++++++++++++++++++- tests/components/snips/test_init.py | 16 ++++++++++++---- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/snips/__init__.py b/homeassistant/components/snips/__init__.py index 70837b95ec5..293caeaedac 100644 --- a/homeassistant/components/snips/__init__.py +++ b/homeassistant/components/snips/__init__.py @@ -7,8 +7,13 @@ import logging import voluptuous as vol from homeassistant.components import mqtt -from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.core import ( + DOMAIN as HOMEASSISTANT_DOMAIN, + HomeAssistant, + ServiceCall, +) from homeassistant.helpers import config_validation as cv, intent +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType DOMAIN = "snips" @@ -91,6 +96,20 @@ SERVICE_SCHEMA_FEEDBACK = vol.Schema( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Activate Snips component.""" + async_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": "Snips", + }, + ) # Make sure MQTT integration is enabled and the client is available if not await mqtt.async_wait_for_mqtt_client(hass): diff --git a/tests/components/snips/test_init.py b/tests/components/snips/test_init.py index 82dbf1cd281..2be6d769f08 100644 --- a/tests/components/snips/test_init.py +++ b/tests/components/snips/test_init.py @@ -7,7 +7,8 @@ import pytest import voluptuous as vol from homeassistant.components import snips -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.intent import ServiceIntentHandler, async_register from homeassistant.setup import async_setup_component @@ -15,9 +16,13 @@ from tests.common import async_fire_mqtt_message, async_mock_intent, async_mock_ from tests.typing import MqttMockHAClient -async def test_snips_config(hass: HomeAssistant, mqtt_mock: MqttMockHAClient) -> None: +async def test_snips_config( + hass: HomeAssistant, + mqtt_mock: MqttMockHAClient, + issue_registry: ir.IssueRegistry, +) -> None: """Test Snips Config.""" - result = await async_setup_component( + assert await async_setup_component( hass, "snips", { @@ -28,7 +33,10 @@ async def test_snips_config(hass: HomeAssistant, mqtt_mock: MqttMockHAClient) -> } }, ) - assert result + assert ( + HOMEASSISTANT_DOMAIN, + f"deprecated_system_packages_yaml_integration_{snips.DOMAIN}", + ) in issue_registry.issues async def test_snips_no_mqtt( From 306bbdc697f1a4a238e4efa42011677f398b3f3b Mon Sep 17 00:00:00 2001 From: Samuel Xiao <40679757+XiaoLing-git@users.noreply.github.com> Date: Sat, 31 May 2025 02:22:40 +0800 Subject: [PATCH 061/266] Bump switchbot-api to 2.4.0 (#145786) * update switchbot-api version to 2.4.0 * debug for test code --- .../components/switchbot_cloud/__init__.py | 12 +++++++++--- .../components/switchbot_cloud/config_flow.py | 10 +++++++--- .../components/switchbot_cloud/coordinator.py | 4 ++-- .../components/switchbot_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/switchbot_cloud/test_config_flow.py | 8 ++++---- tests/components/switchbot_cloud/test_init.py | 14 ++++++++++---- 8 files changed, 35 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/switchbot_cloud/__init__.py b/homeassistant/components/switchbot_cloud/__init__.py index c7bf66a5803..7b7f60589f0 100644 --- a/homeassistant/components/switchbot_cloud/__init__.py +++ b/homeassistant/components/switchbot_cloud/__init__.py @@ -7,7 +7,13 @@ from dataclasses import dataclass, field from logging import getLogger from aiohttp import web -from switchbot_api import CannotConnect, Device, InvalidAuth, Remote, SwitchBotAPI +from switchbot_api import ( + Device, + Remote, + SwitchBotAPI, + SwitchBotAuthenticationError, + SwitchBotConnectionError, +) from homeassistant.components import webhook from homeassistant.config_entries import ConfigEntry @@ -175,12 +181,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: api = SwitchBotAPI(token=token, secret=secret) try: devices = await api.list_devices() - except InvalidAuth as ex: + except SwitchBotAuthenticationError as ex: _LOGGER.error( "Invalid authentication while connecting to SwitchBot API: %s", ex ) return False - except CannotConnect as ex: + except SwitchBotConnectionError as ex: raise ConfigEntryNotReady from ex _LOGGER.debug("Devices: %s", devices) coordinators_by_id: dict[str, SwitchBotCoordinator] = {} diff --git a/homeassistant/components/switchbot_cloud/config_flow.py b/homeassistant/components/switchbot_cloud/config_flow.py index eafe823bc0b..0ba1e0295e0 100644 --- a/homeassistant/components/switchbot_cloud/config_flow.py +++ b/homeassistant/components/switchbot_cloud/config_flow.py @@ -3,7 +3,11 @@ from logging import getLogger from typing import Any -from switchbot_api import CannotConnect, InvalidAuth, SwitchBotAPI +from switchbot_api import ( + SwitchBotAPI, + SwitchBotAuthenticationError, + SwitchBotConnectionError, +) import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult @@ -36,9 +40,9 @@ class SwitchBotCloudConfigFlow(ConfigFlow, domain=DOMAIN): await SwitchBotAPI( token=user_input[CONF_API_TOKEN], secret=user_input[CONF_API_KEY] ).list_devices() - except CannotConnect: + except SwitchBotConnectionError: errors["base"] = "cannot_connect" - except InvalidAuth: + except SwitchBotAuthenticationError: errors["base"] = "invalid_auth" except Exception: _LOGGER.exception("Unexpected exception") diff --git a/homeassistant/components/switchbot_cloud/coordinator.py b/homeassistant/components/switchbot_cloud/coordinator.py index 4f047145b47..9fc8f64aa68 100644 --- a/homeassistant/components/switchbot_cloud/coordinator.py +++ b/homeassistant/components/switchbot_cloud/coordinator.py @@ -4,7 +4,7 @@ from asyncio import timeout from logging import getLogger from typing import Any -from switchbot_api import CannotConnect, Device, Remote, SwitchBotAPI +from switchbot_api import Device, Remote, SwitchBotAPI, SwitchBotConnectionError from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -70,5 +70,5 @@ class SwitchBotCoordinator(DataUpdateCoordinator[Status]): status: Status = await self._api.get_status(self._device_id) _LOGGER.debug("Refreshing %s with %s", self._device_id, status) return status - except CannotConnect as err: + except SwitchBotConnectionError as err: raise UpdateFailed(f"Error communicating with API: {err}") from err diff --git a/homeassistant/components/switchbot_cloud/manifest.json b/homeassistant/components/switchbot_cloud/manifest.json index 83404aac2ba..e0c49d9e739 100644 --- a/homeassistant/components/switchbot_cloud/manifest.json +++ b/homeassistant/components/switchbot_cloud/manifest.json @@ -8,5 +8,5 @@ "integration_type": "hub", "iot_class": "cloud_polling", "loggers": ["switchbot_api"], - "requirements": ["switchbot-api==2.3.1"] + "requirements": ["switchbot-api==2.4.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 68ec08b1c90..eae64f1225a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2859,7 +2859,7 @@ surepy==0.9.0 swisshydrodata==0.1.0 # homeassistant.components.switchbot_cloud -switchbot-api==2.3.1 +switchbot-api==2.4.0 # homeassistant.components.synology_srm synology-srm==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 77661e0f624..002189807fa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2354,7 +2354,7 @@ subarulink==0.7.13 surepy==0.9.0 # homeassistant.components.switchbot_cloud -switchbot-api==2.3.1 +switchbot-api==2.4.0 # homeassistant.components.system_bridge systembridgeconnector==4.1.5 diff --git a/tests/components/switchbot_cloud/test_config_flow.py b/tests/components/switchbot_cloud/test_config_flow.py index 1d49b503ef2..5eef1805a5a 100644 --- a/tests/components/switchbot_cloud/test_config_flow.py +++ b/tests/components/switchbot_cloud/test_config_flow.py @@ -6,8 +6,8 @@ import pytest from homeassistant import config_entries from homeassistant.components.switchbot_cloud.config_flow import ( - CannotConnect, - InvalidAuth, + SwitchBotAuthenticationError, + SwitchBotConnectionError, ) from homeassistant.components.switchbot_cloud.const import DOMAIN, ENTRY_TITLE from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN @@ -57,8 +57,8 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: @pytest.mark.parametrize( ("error", "message"), [ - (InvalidAuth, "invalid_auth"), - (CannotConnect, "cannot_connect"), + (SwitchBotAuthenticationError, "invalid_auth"), + (SwitchBotConnectionError, "cannot_connect"), (Exception, "unknown"), ], ) diff --git a/tests/components/switchbot_cloud/test_init.py b/tests/components/switchbot_cloud/test_init.py index bab9200e7c9..b55106e90d9 100644 --- a/tests/components/switchbot_cloud/test_init.py +++ b/tests/components/switchbot_cloud/test_init.py @@ -3,7 +3,13 @@ from unittest.mock import patch import pytest -from switchbot_api import CannotConnect, Device, InvalidAuth, PowerState, Remote +from switchbot_api import ( + Device, + PowerState, + Remote, + SwitchBotAuthenticationError, + SwitchBotConnectionError, +) from homeassistant.components.switchbot_cloud import SwitchBotAPI from homeassistant.config_entries import ConfigEntryState @@ -127,8 +133,8 @@ async def test_setup_entry_success( @pytest.mark.parametrize( ("error", "state"), [ - (InvalidAuth, ConfigEntryState.SETUP_ERROR), - (CannotConnect, ConfigEntryState.SETUP_RETRY), + (SwitchBotAuthenticationError, ConfigEntryState.SETUP_ERROR), + (SwitchBotConnectionError, ConfigEntryState.SETUP_RETRY), ], ) async def test_setup_entry_fails_when_listing_devices( @@ -162,7 +168,7 @@ async def test_setup_entry_fails_when_refreshing( hubDeviceId="test-hub-id", ) ] - mock_get_status.side_effect = CannotConnect + mock_get_status.side_effect = SwitchBotConnectionError entry = await configure_integration(hass) assert entry.state is ConfigEntryState.SETUP_RETRY From c84ffb54d2fe5b0c9aca802ab269d50dbbd76b8c Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Sat, 31 May 2025 04:21:51 +1000 Subject: [PATCH 062/266] Bump tesla-fleet-api to 1.1.1. (#145869) bump --- homeassistant/components/tesla_fleet/manifest.json | 2 +- homeassistant/components/teslemetry/manifest.json | 2 +- homeassistant/components/tessie/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/tesla_fleet/manifest.json b/homeassistant/components/tesla_fleet/manifest.json index 53c8e7d554c..8f5ba1468a5 100644 --- a/homeassistant/components/tesla_fleet/manifest.json +++ b/homeassistant/components/tesla_fleet/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/tesla_fleet", "iot_class": "cloud_polling", "loggers": ["tesla-fleet-api"], - "requirements": ["tesla-fleet-api==1.0.17"] + "requirements": ["tesla-fleet-api==1.1.1"] } diff --git a/homeassistant/components/teslemetry/manifest.json b/homeassistant/components/teslemetry/manifest.json index 855cdc9f364..7fc621eeeae 100644 --- a/homeassistant/components/teslemetry/manifest.json +++ b/homeassistant/components/teslemetry/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/teslemetry", "iot_class": "cloud_polling", "loggers": ["tesla-fleet-api"], - "requirements": ["tesla-fleet-api==1.0.17", "teslemetry-stream==0.7.9"] + "requirements": ["tesla-fleet-api==1.1.1", "teslemetry-stream==0.7.9"] } diff --git a/homeassistant/components/tessie/manifest.json b/homeassistant/components/tessie/manifest.json index 3f71bcb95e3..9ad87e9dbbe 100644 --- a/homeassistant/components/tessie/manifest.json +++ b/homeassistant/components/tessie/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/tessie", "iot_class": "cloud_polling", "loggers": ["tessie", "tesla-fleet-api"], - "requirements": ["tessie-api==0.1.1", "tesla-fleet-api==1.0.17"] + "requirements": ["tessie-api==0.1.1", "tesla-fleet-api==1.1.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index eae64f1225a..997184849ae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2900,7 +2900,7 @@ temperusb==1.6.1 # homeassistant.components.tesla_fleet # homeassistant.components.teslemetry # homeassistant.components.tessie -tesla-fleet-api==1.0.17 +tesla-fleet-api==1.1.1 # homeassistant.components.powerwall tesla-powerwall==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 002189807fa..66637db0537 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2380,7 +2380,7 @@ temperusb==1.6.1 # homeassistant.components.tesla_fleet # homeassistant.components.teslemetry # homeassistant.components.tessie -tesla-fleet-api==1.0.17 +tesla-fleet-api==1.1.1 # homeassistant.components.powerwall tesla-powerwall==0.5.2 From fb2d8c640643120bc228951c07ba15b81e601848 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Sun, 1 Jun 2025 04:01:10 +1000 Subject: [PATCH 063/266] Add streaming to charge cable connected in Teslemetry (#145880) --- homeassistant/components/teslemetry/binary_sensor.py | 3 +++ tests/components/teslemetry/snapshots/test_binary_sensor.ambr | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/teslemetry/binary_sensor.py b/homeassistant/components/teslemetry/binary_sensor.py index 99c21cbe03e..a32c5fea40e 100644 --- a/homeassistant/components/teslemetry/binary_sensor.py +++ b/homeassistant/components/teslemetry/binary_sensor.py @@ -125,6 +125,9 @@ 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 != "Unknown") + ), entity_category=EntityCategory.DIAGNOSTIC, device_class=BinarySensorDeviceClass.CONNECTIVITY, ), diff --git a/tests/components/teslemetry/snapshots/test_binary_sensor.ambr b/tests/components/teslemetry/snapshots/test_binary_sensor.ambr index 8bcd837d06f..06ec0a60434 100644 --- a/tests/components/teslemetry/snapshots/test_binary_sensor.ambr +++ b/tests/components/teslemetry/snapshots/test_binary_sensor.ambr @@ -673,7 +673,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'on', + 'state': 'unknown', }) # --- # name: test_binary_sensor[binary_sensor.test_charge_enable_request-entry] @@ -3374,7 +3374,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'on', + 'state': 'unknown', }) # --- # name: test_binary_sensor_refresh[binary_sensor.test_charge_enable_request-statealt] From a6608bd7ea8145fe1f81b353a65a4deb5264cc1b Mon Sep 17 00:00:00 2001 From: Iskra kranj <162285659+iskrakranj@users.noreply.github.com> Date: Fri, 30 May 2025 20:21:14 +0200 Subject: [PATCH 064/266] Bump pyiskra to 0.1.19 (#145889) --- homeassistant/components/iskra/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/iskra/manifest.json b/homeassistant/components/iskra/manifest.json index caa176ab6b6..3f7c805a917 100644 --- a/homeassistant/components/iskra/manifest.json +++ b/homeassistant/components/iskra/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["pyiskra"], - "requirements": ["pyiskra==0.1.15"] + "requirements": ["pyiskra==0.1.19"] } diff --git a/requirements_all.txt b/requirements_all.txt index 997184849ae..9735bbc48bb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2051,7 +2051,7 @@ pyiqvia==2022.04.0 pyirishrail==0.0.2 # homeassistant.components.iskra -pyiskra==0.1.15 +pyiskra==0.1.19 # homeassistant.components.iss pyiss==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 66637db0537..18d2be2057e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1699,7 +1699,7 @@ pyipp==0.17.0 pyiqvia==2022.04.0 # homeassistant.components.iskra -pyiskra==0.1.15 +pyiskra==0.1.19 # homeassistant.components.iss pyiss==1.0.1 From 6015f60db4d9554a82169082aa95d3fa052f019f Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Fri, 30 May 2025 19:35:08 +0200 Subject: [PATCH 065/266] Bump python-linkplay to v0.2.9 (#145892) --- homeassistant/components/linkplay/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/linkplay/manifest.json b/homeassistant/components/linkplay/manifest.json index fafc9e66514..eb9b5a87c75 100644 --- a/homeassistant/components/linkplay/manifest.json +++ b/homeassistant/components/linkplay/manifest.json @@ -7,6 +7,6 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["linkplay"], - "requirements": ["python-linkplay==0.2.8"], + "requirements": ["python-linkplay==0.2.9"], "zeroconf": ["_linkplay._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 9735bbc48bb..84739a2500c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2452,7 +2452,7 @@ python-juicenet==1.1.0 python-kasa[speedups]==0.10.2 # homeassistant.components.linkplay -python-linkplay==0.2.8 +python-linkplay==0.2.9 # homeassistant.components.lirc # python-lirc==1.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 18d2be2057e..70b1190a7eb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2019,7 +2019,7 @@ python-juicenet==1.1.0 python-kasa[speedups]==0.10.2 # homeassistant.components.linkplay -python-linkplay==0.2.8 +python-linkplay==0.2.9 # homeassistant.components.lirc # python-lirc==1.2.3 From ddc79a631d2ba39ce786658e514de42a109da5c6 Mon Sep 17 00:00:00 2001 From: Jordan Harvey Date: Fri, 30 May 2025 18:33:03 +0100 Subject: [PATCH 066/266] Bump pyprobeplus to 1.0.1 (#145897) --- homeassistant/components/probe_plus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/probe_plus/manifest.json b/homeassistant/components/probe_plus/manifest.json index cf61e394a83..e7db39b8ae4 100644 --- a/homeassistant/components/probe_plus/manifest.json +++ b/homeassistant/components/probe_plus/manifest.json @@ -15,5 +15,5 @@ "integration_type": "device", "iot_class": "local_push", "quality_scale": "bronze", - "requirements": ["pyprobeplus==1.0.0"] + "requirements": ["pyprobeplus==1.0.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 84739a2500c..e4b150bbf89 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2251,7 +2251,7 @@ pyplaato==0.0.19 pypoint==3.0.0 # homeassistant.components.probe_plus -pyprobeplus==1.0.0 +pyprobeplus==1.0.1 # homeassistant.components.profiler pyprof2calltree==1.4.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 70b1190a7eb..59d4d9a2db3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1869,7 +1869,7 @@ pyplaato==0.0.19 pypoint==3.0.0 # homeassistant.components.probe_plus -pyprobeplus==1.0.0 +pyprobeplus==1.0.1 # homeassistant.components.profiler pyprof2calltree==1.4.5 From d0bf9d9bfb610c84b467167c2bf8fb9626e8b887 Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Sat, 31 May 2025 11:19:32 +0200 Subject: [PATCH 067/266] Move server device creation to init in jellyfin (#145910) * Move server device creation to init in jellyfin * move device creation to after coordinator refresh --- homeassistant/components/jellyfin/__init__.py | 13 +++++++++++-- homeassistant/components/jellyfin/entity.py | 8 ++------ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/jellyfin/__init__.py b/homeassistant/components/jellyfin/__init__.py index 1cb6219ada0..d22594070ff 100644 --- a/homeassistant/components/jellyfin/__init__.py +++ b/homeassistant/components/jellyfin/__init__.py @@ -7,7 +7,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import device_registry as dr from .client_wrapper import CannotConnect, InvalidAuth, create_client, validate_input -from .const import CONF_CLIENT_DEVICE_ID, DOMAIN, PLATFORMS +from .const import CONF_CLIENT_DEVICE_ID, DEFAULT_NAME, DOMAIN, PLATFORMS from .coordinator import JellyfinConfigEntry, JellyfinDataUpdateCoordinator @@ -35,9 +35,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: JellyfinConfigEntry) -> coordinator = JellyfinDataUpdateCoordinator( hass, entry, client, server_info, user_id ) - await coordinator.async_config_entry_first_refresh() + device_registry = dr.async_get(hass) + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + entry_type=dr.DeviceEntryType.SERVICE, + identifiers={(DOMAIN, coordinator.server_id)}, + manufacturer=DEFAULT_NAME, + name=coordinator.server_name, + sw_version=coordinator.server_version, + ) + entry.runtime_data = coordinator entry.async_on_unload(client.stop) diff --git a/homeassistant/components/jellyfin/entity.py b/homeassistant/components/jellyfin/entity.py index 4a3b2b77bb1..107a67d6a89 100644 --- a/homeassistant/components/jellyfin/entity.py +++ b/homeassistant/components/jellyfin/entity.py @@ -4,10 +4,10 @@ from __future__ import annotations from typing import Any -from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DEFAULT_NAME, DOMAIN +from .const import DOMAIN from .coordinator import JellyfinDataUpdateCoordinator @@ -24,11 +24,7 @@ class JellyfinServerEntity(JellyfinEntity): """Initialize the Jellyfin entity.""" super().__init__(coordinator) self._attr_device_info = DeviceInfo( - entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, coordinator.server_id)}, - manufacturer=DEFAULT_NAME, - name=coordinator.server_name, - sw_version=coordinator.server_version, ) From cd905a65934b649d33ff75e10c54b9255a7ef49d Mon Sep 17 00:00:00 2001 From: tronikos Date: Sat, 31 May 2025 02:22:49 -0700 Subject: [PATCH 068/266] Bump opower to 0.12.3 (#145918) --- homeassistant/components/opower/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/opower/manifest.json b/homeassistant/components/opower/manifest.json index 7ac9f4cc943..0aa26dbb4b1 100644 --- a/homeassistant/components/opower/manifest.json +++ b/homeassistant/components/opower/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/opower", "iot_class": "cloud_polling", "loggers": ["opower"], - "requirements": ["opower==0.12.2"] + "requirements": ["opower==0.12.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index e4b150bbf89..a1ad1673b67 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1617,7 +1617,7 @@ openwrt-luci-rpc==1.1.17 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.12.2 +opower==0.12.3 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 59d4d9a2db3..3a989d3c2b7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1367,7 +1367,7 @@ openhomedevice==2.2.0 openwebifpy==4.3.1 # homeassistant.components.opower -opower==0.12.2 +opower==0.12.3 # homeassistant.components.oralb oralb-ble==0.17.6 From 532c077ddf985f72c1ef8dfff34c7b5f2353954e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 31 May 2025 04:12:00 -0500 Subject: [PATCH 069/266] Bump aiohttp to 3.12.6 (#145919) * Bump aiohttp to 3.12.5 changelog: https://github.com/aio-libs/aiohttp/compare/v3.12.4...v3.12.5 * .6 * fix mock --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- tests/components/hassio/conftest.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 8dde4a6a654..3a70b1ff8e8 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -6,7 +6,7 @@ aiodns==3.4.0 aiohasupervisor==0.3.1 aiohttp-asyncmdnsresolver==0.1.1 aiohttp-fast-zlib==0.2.3 -aiohttp==3.12.4 +aiohttp==3.12.6 aiohttp_cors==0.7.0 aiousbwatcher==1.1.1 aiozoneinfo==0.2.3 diff --git a/pyproject.toml b/pyproject.toml index dc185344ca8..791b616b0c3 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.4", + "aiohttp==3.12.6", "aiohttp_cors==0.7.0", "aiohttp-fast-zlib==0.2.3", "aiohttp-asyncmdnsresolver==0.1.1", diff --git a/requirements.txt b/requirements.txt index f1ced51637e..a9a3e105f33 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ # Home Assistant Core aiodns==3.4.0 aiohasupervisor==0.3.1 -aiohttp==3.12.4 +aiohttp==3.12.6 aiohttp_cors==0.7.0 aiohttp-fast-zlib==0.2.3 aiohttp-asyncmdnsresolver==0.1.1 diff --git a/tests/components/hassio/conftest.py b/tests/components/hassio/conftest.py index ea38865ac5a..a71ee370b32 100644 --- a/tests/components/hassio/conftest.py +++ b/tests/components/hassio/conftest.py @@ -63,7 +63,7 @@ async def hassio_client_supervisor( @pytest.fixture -def hassio_handler( +async def hassio_handler( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> Generator[HassIO]: """Create mock hassio handler.""" From ef0b3c9f9c482df884e2660b962a4f64d4113c99 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sat, 31 May 2025 17:54:36 +0200 Subject: [PATCH 070/266] Update frontend to 20250531.0 (#145933) --- 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 3ee40e1ce60..7282482f329 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==20250528.0"] + "requirements": ["home-assistant-frontend==20250531.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3a70b1ff8e8..5aa0d8ae82b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ habluetooth==3.48.2 hass-nabucasa==0.101.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250528.0 +home-assistant-frontend==20250531.0 home-assistant-intents==2025.5.28 httpx==0.28.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index a1ad1673b67..fc61430c302 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1164,7 +1164,7 @@ hole==0.8.0 holidays==0.73 # homeassistant.components.frontend -home-assistant-frontend==20250528.0 +home-assistant-frontend==20250531.0 # homeassistant.components.conversation home-assistant-intents==2025.5.28 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3a989d3c2b7..81cb7278ec1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1007,7 +1007,7 @@ hole==0.8.0 holidays==0.73 # homeassistant.components.frontend -home-assistant-frontend==20250528.0 +home-assistant-frontend==20250531.0 # homeassistant.components.conversation home-assistant-intents==2025.5.28 From 745902bc7e92a2075f46e6958265502fee297c48 Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Sat, 31 May 2025 20:25:47 +0200 Subject: [PATCH 071/266] Bump pylamarzocco to 2.0.8 (#145938) --- 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 36a0a489e30..46a29427264 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.7"] + "requirements": ["pylamarzocco==2.0.8"] } diff --git a/requirements_all.txt b/requirements_all.txt index fc61430c302..63ddad956e9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2096,7 +2096,7 @@ pykwb==0.0.8 pylacrosse==0.4 # homeassistant.components.lamarzocco -pylamarzocco==2.0.7 +pylamarzocco==2.0.8 # homeassistant.components.lastfm pylast==5.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 81cb7278ec1..c7c1f182cd2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1735,7 +1735,7 @@ pykrakenapi==0.1.8 pykulersky==0.5.8 # homeassistant.components.lamarzocco -pylamarzocco==2.0.7 +pylamarzocco==2.0.8 # homeassistant.components.lastfm pylast==5.1.0 From 907cebdd6d57107e1c8ee2597d2de03662124198 Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Sat, 31 May 2025 20:25:57 +0200 Subject: [PATCH 072/266] Increase update intervals in lamarzocco (#145939) --- homeassistant/components/lamarzocco/coordinator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lamarzocco/coordinator.py b/homeassistant/components/lamarzocco/coordinator.py index f0f64e02c28..b6379f237ae 100644 --- a/homeassistant/components/lamarzocco/coordinator.py +++ b/homeassistant/components/lamarzocco/coordinator.py @@ -20,8 +20,8 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import DOMAIN SCAN_INTERVAL = timedelta(seconds=15) -SETTINGS_UPDATE_INTERVAL = timedelta(hours=1) -SCHEDULE_UPDATE_INTERVAL = timedelta(minutes=5) +SETTINGS_UPDATE_INTERVAL = timedelta(hours=8) +SCHEDULE_UPDATE_INTERVAL = timedelta(minutes=30) STATISTICS_UPDATE_INTERVAL = timedelta(minutes=15) _LOGGER = logging.getLogger(__name__) From 06d869aaa5efdfddd2a10648ac9c94172871cb37 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sat, 31 May 2025 21:25:06 +0200 Subject: [PATCH 073/266] Bump version to 2025.6.0b4 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 29095678e48..edbdba419f3 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 = 6 -PATCH_VERSION: Final = "0b3" +PATCH_VERSION: Final = "0b4" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index 791b616b0c3..aa9de97d73b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.6.0b3" +version = "2025.6.0b4" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From ea6b9e5260c74aa00ad172fb65e54a67ce1f74fc Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Tue, 3 Jun 2025 12:12:56 +0200 Subject: [PATCH 074/266] SMA add missing strings for DHCP (#145782) --- homeassistant/components/sma/config_flow.py | 1 + homeassistant/components/sma/strings.json | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/homeassistant/components/sma/config_flow.py b/homeassistant/components/sma/config_flow.py index c920b4b0a3a..f43c851d04a 100644 --- a/homeassistant/components/sma/config_flow.py +++ b/homeassistant/components/sma/config_flow.py @@ -218,5 +218,6 @@ class SmaConfigFlow(ConfigFlow, domain=DOMAIN): vol.Required(CONF_PASSWORD): cv.string, } ), + description_placeholders={CONF_HOST: self._data[CONF_HOST]}, errors=errors, ) diff --git a/homeassistant/components/sma/strings.json b/homeassistant/components/sma/strings.json index 3a7c87acfcc..e19acf20cf8 100644 --- a/homeassistant/components/sma/strings.json +++ b/homeassistant/components/sma/strings.json @@ -32,6 +32,16 @@ }, "description": "Enter your SMA device information.", "title": "Set up SMA Solar" + }, + "discovery_confirm": { + "title": "[%key:component::sma::config::step::user::title]", + "description": "Do you want to setup the discovered SMA ({host})?", + "data": { + "group": "[%key:component::sma::config::step::user::data::group]", + "password": "[%key:common::config_flow::data::password%]", + "ssl": "[%key:common::config_flow::data::ssl%]", + "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" + } } } } From b1d35de8e4612266d722de36b23492f0be1b3df9 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 4 Jun 2025 10:00:50 +0200 Subject: [PATCH 075/266] Deprecate eddystone temperature integration (#145833) --- .../eddystone_temperature/__init__.py | 5 +++ .../eddystone_temperature/sensor.py | 24 ++++++++-- requirements_test_all.txt | 3 ++ .../eddystone_temperature/__init__.py | 1 + .../eddystone_temperature/test_sensor.py | 45 +++++++++++++++++++ 5 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 tests/components/eddystone_temperature/__init__.py create mode 100644 tests/components/eddystone_temperature/test_sensor.py diff --git a/homeassistant/components/eddystone_temperature/__init__.py b/homeassistant/components/eddystone_temperature/__init__.py index 2d6f92498bd..af37eb629b5 100644 --- a/homeassistant/components/eddystone_temperature/__init__.py +++ b/homeassistant/components/eddystone_temperature/__init__.py @@ -1 +1,6 @@ """The eddystone_temperature component.""" + +DOMAIN = "eddystone_temperature" +CONF_BEACONS = "beacons" +CONF_INSTANCE = "instance" +CONF_NAMESPACE = "namespace" diff --git a/homeassistant/components/eddystone_temperature/sensor.py b/homeassistant/components/eddystone_temperature/sensor.py index 1047c52e111..7b8e726cf45 100644 --- a/homeassistant/components/eddystone_temperature/sensor.py +++ b/homeassistant/components/eddystone_temperature/sensor.py @@ -23,17 +23,18 @@ from homeassistant.const import ( STATE_UNKNOWN, UnitOfTemperature, ) -from homeassistant.core import Event, HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, Event, 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 CONF_BEACONS, CONF_INSTANCE, CONF_NAMESPACE, DOMAIN + _LOGGER = logging.getLogger(__name__) -CONF_BEACONS = "beacons" CONF_BT_DEVICE_ID = "bt_device_id" -CONF_INSTANCE = "instance" -CONF_NAMESPACE = "namespace" + BEACON_SCHEMA = vol.Schema( { @@ -58,6 +59,21 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Validate configuration, create devices and start monitoring thread.""" + 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": "Eddystone", + }, + ) + bt_device_id: int = config[CONF_BT_DEVICE_ID] beacons: dict[str, dict[str, str]] = config[CONF_BEACONS] diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c7c1f182cd2..e3f5788e5d8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -533,6 +533,9 @@ babel==2.15.0 # homeassistant.components.homekit base36==0.1.1 +# homeassistant.components.eddystone_temperature +# beacontools[scan]==2.1.0 + # homeassistant.components.scrape beautifulsoup4==4.13.3 diff --git a/tests/components/eddystone_temperature/__init__.py b/tests/components/eddystone_temperature/__init__.py new file mode 100644 index 00000000000..af67530c946 --- /dev/null +++ b/tests/components/eddystone_temperature/__init__.py @@ -0,0 +1 @@ +"""Tests for eddystone temperature.""" diff --git a/tests/components/eddystone_temperature/test_sensor.py b/tests/components/eddystone_temperature/test_sensor.py new file mode 100644 index 00000000000..056681fdb90 --- /dev/null +++ b/tests/components/eddystone_temperature/test_sensor.py @@ -0,0 +1,45 @@ +"""Tests for eddystone temperature.""" + +from unittest.mock import Mock, patch + +from homeassistant.components.eddystone_temperature import ( + CONF_BEACONS, + CONF_INSTANCE, + CONF_NAMESPACE, + DOMAIN, +) +from homeassistant.components.sensor import DOMAIN as PLATFORM_DOMAIN +from homeassistant.const import CONF_PLATFORM +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.helpers import issue_registry as ir +from homeassistant.setup import async_setup_component + + +@patch.dict("sys.modules", beacontools=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, + { + PLATFORM_DOMAIN: [ + { + CONF_PLATFORM: DOMAIN, + CONF_BEACONS: { + "living_room": { + CONF_NAMESPACE: "112233445566778899AA", + CONF_INSTANCE: "000000000001", + } + }, + } + ], + }, + ) + await hass.async_block_till_done() + assert ( + HOMEASSISTANT_DOMAIN, + f"deprecated_system_packages_yaml_integration_{DOMAIN}", + ) in issue_registry.issues From 03f028b7e290c8ed5e1449573b77491fc5ede746 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Mon, 2 Jun 2025 09:45:14 +0200 Subject: [PATCH 076/266] Deprecate hddtemp (#145850) --- homeassistant/components/hddtemp/__init__.py | 2 ++ homeassistant/components/hddtemp/sensor.py | 20 ++++++++++++++++++- tests/components/hddtemp/test_sensor.py | 21 ++++++++++++++++++-- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hddtemp/__init__.py b/homeassistant/components/hddtemp/__init__.py index 121238df9fe..66a819f1e8d 100644 --- a/homeassistant/components/hddtemp/__init__.py +++ b/homeassistant/components/hddtemp/__init__.py @@ -1 +1,3 @@ """The hddtemp component.""" + +DOMAIN = "hddtemp" diff --git a/homeassistant/components/hddtemp/sensor.py b/homeassistant/components/hddtemp/sensor.py index 4d9bbeb9516..192ddffd330 100644 --- a/homeassistant/components/hddtemp/sensor.py +++ b/homeassistant/components/hddtemp/sensor.py @@ -22,11 +22,14 @@ from homeassistant.const import ( CONF_PORT, UnitOfTemperature, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, 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" @@ -56,6 +59,21 @@ 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 56ad9fdcb0e..62882c7df8b 100644 --- a/tests/components/hddtemp/test_sensor.py +++ b/tests/components/hddtemp/test_sensor.py @@ -1,12 +1,15 @@ """The tests for the hddtemp platform.""" import socket -from unittest.mock import patch +from unittest.mock import Mock, 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 HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.helpers import issue_registry as ir from homeassistant.setup import async_setup_component VALID_CONFIG_MINIMAL = {"sensor": {"platform": "hddtemp"}} @@ -192,3 +195,17 @@ 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 1e1b0424d74c8168226b38d1c3da20975b2f1c9c Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 2 Jun 2025 09:52:02 +0200 Subject: [PATCH 077/266] Fix removal of devices during Z-Wave migration (#145867) --- homeassistant/components/zwave_js/__init__.py | 8 +- .../components/zwave_js/config_flow.py | 21 +++ homeassistant/components/zwave_js/const.py | 1 + tests/components/zwave_js/test_config_flow.py | 130 +++++++++++++++--- 4 files changed, 135 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 6e76b2f89cf..abbf10fb494 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -94,6 +94,7 @@ from .const import ( CONF_DATA_COLLECTION_OPTED_IN, CONF_INSTALLER_MODE, CONF_INTEGRATION_CREATED_ADDON, + CONF_KEEP_OLD_DEVICES, CONF_LR_S2_ACCESS_CONTROL_KEY, CONF_LR_S2_AUTHENTICATED_KEY, CONF_NETWORK_KEY, @@ -405,9 +406,10 @@ class DriverEvents: # Devices that are in the device registry that are not known by the controller # can be removed - for device in stored_devices: - if device not in known_devices and device not in provisioned_devices: - self.dev_reg.async_remove_device(device.id) + if not self.config_entry.data.get(CONF_KEEP_OLD_DEVICES): + for device in stored_devices: + if device not in known_devices and device not in provisioned_devices: + self.dev_reg.async_remove_device(device.id) # run discovery on controller node if controller.own_node: diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index e2941b52522..08c9ec2e2b2 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -56,6 +56,7 @@ from .const import ( CONF_ADDON_S2_AUTHENTICATED_KEY, CONF_ADDON_S2_UNAUTHENTICATED_KEY, CONF_INTEGRATION_CREATED_ADDON, + CONF_KEEP_OLD_DEVICES, CONF_LR_S2_ACCESS_CONTROL_KEY, CONF_LR_S2_AUTHENTICATED_KEY, CONF_S0_LEGACY_KEY, @@ -1383,9 +1384,20 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): config_entry = self._reconfigure_config_entry assert config_entry is not None + # Make sure we keep the old devices + # so that user customizations are not lost, + # when loading the config entry. + self.hass.config_entries.async_update_entry( + config_entry, data=config_entry.data | {CONF_KEEP_OLD_DEVICES: True} + ) + # Reload the config entry to reconnect the client after the addon restart await self.hass.config_entries.async_reload(config_entry.entry_id) + data = config_entry.data.copy() + data.pop(CONF_KEEP_OLD_DEVICES, None) + self.hass.config_entries.async_update_entry(config_entry, data=data) + @callback def forward_progress(event: dict) -> None: """Forward progress events to frontend.""" @@ -1436,6 +1448,15 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): 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. + # 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, + # but not immediately remove the old device. + await self.hass.config_entries.async_reload(config_entry.entry_id) + finally: for unsub in unsubs: unsub() diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index 31cfb144e2a..6d5cbb98902 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -27,6 +27,7 @@ CONF_ADDON_LR_S2_ACCESS_CONTROL_KEY = "lr_s2_access_control_key" CONF_ADDON_LR_S2_AUTHENTICATED_KEY = "lr_s2_authenticated_key" CONF_INSTALLER_MODE = "installer_mode" CONF_INTEGRATION_CREATED_ADDON = "integration_created_addon" +CONF_KEEP_OLD_DEVICES = "keep_old_devices" CONF_NETWORK_KEY = "network_key" CONF_S0_LEGACY_KEY = "s0_legacy_key" CONF_S2_ACCESS_CONTROL_KEY = "s2_access_control_key" diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index c9929759a49..fc01c9b29b1 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -15,6 +15,7 @@ import pytest from serial.tools.list_ports_common import ListPortInfo from voluptuous import InInvalid from zwave_js_server.exceptions import FailedCommand +from zwave_js_server.model.node import Node from zwave_js_server.version import VersionInfo from homeassistant import config_entries, data_entry_flow @@ -40,6 +41,7 @@ from homeassistant.components.zwave_js.const import ( from homeassistant.components.zwave_js.helpers import SERVER_VERSION_TIMEOUT from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.service_info.hassio import HassioServiceInfo from homeassistant.helpers.service_info.usb import UsbServiceInfo from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo @@ -969,7 +971,7 @@ async def test_usb_discovery_migration( assert client.connect.call_count == 2 await hass.async_block_till_done() - assert client.connect.call_count == 3 + assert client.connect.call_count == 4 assert entry.state is config_entries.ConfigEntryState.LOADED assert client.driver.controller.async_restore_nvm.call_count == 1 assert len(events) == 2 @@ -983,6 +985,7 @@ async def test_usb_discovery_migration( assert entry.data["url"] == "ws://host1:3001" assert entry.data["usb_path"] == USB_DISCOVERY_INFO.device assert entry.data["use_addon"] is True + assert "keep_old_devices" not in entry.data assert entry.unique_id == "5678" @@ -1097,7 +1100,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 == 3 + assert client.connect.call_count == 4 assert entry.state is config_entries.ConfigEntryState.LOADED assert client.driver.controller.async_restore_nvm.call_count == 1 assert len(events) == 2 @@ -1108,9 +1111,10 @@ async def test_usb_discovery_migration_restore_driver_ready_timeout( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "migration_successful" - assert integration.data["url"] == "ws://host1:3001" - assert integration.data["usb_path"] == USB_DISCOVERY_INFO.device - assert integration.data["use_addon"] is True + assert entry.data["url"] == "ws://host1:3001" + assert entry.data["usb_path"] == USB_DISCOVERY_INFO.device + assert entry.data["use_addon"] is True + assert "keep_old_devices" not in entry.data @pytest.mark.usefixtures("supervisor", "addon_installed") @@ -3422,6 +3426,7 @@ async def test_reconfigure_migrate_no_addon( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "addon_required" + assert "keep_old_devices" not in entry.data @pytest.mark.usefixtures("mock_sdk_version") @@ -3446,6 +3451,7 @@ async def test_reconfigure_migrate_low_sdk_version( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "migration_low_sdk_version" + assert "keep_old_devices" not in entry.data @pytest.mark.usefixtures("supervisor", "addon_running") @@ -3457,15 +3463,22 @@ async def test_reconfigure_migrate_low_sdk_version( "final_unique_id", ), [ - (None, "4321", None, "8765"), - (aiohttp.ClientError("Boom"), "1234", None, "8765"), + (None, "4321", None, "3245146787"), + (aiohttp.ClientError("Boom"), "3245146787", None, "3245146787"), (None, "4321", aiohttp.ClientError("Boom"), "5678"), - (aiohttp.ClientError("Boom"), "1234", aiohttp.ClientError("Boom"), "5678"), + ( + aiohttp.ClientError("Boom"), + "3245146787", + aiohttp.ClientError("Boom"), + "5678", + ), ], ) async def test_reconfigure_migrate_with_addon( hass: HomeAssistant, client: MagicMock, + device_registry: dr.DeviceRegistry, + multisensor_6: Node, integration: MockConfigEntry, restart_addon: AsyncMock, addon_options: dict[str, Any], @@ -3482,9 +3495,9 @@ async def test_reconfigure_migrate_with_addon( version_info.home_id = 4321 entry = integration assert client.connect.call_count == 1 + assert client.driver.controller.home_id == 3245146787 hass.config_entries.async_update_entry( entry, - unique_id="1234", data={ "url": "ws://localhost:3000", "use_addon": True, @@ -3493,6 +3506,39 @@ async def test_reconfigure_migrate_with_addon( ) addon_options["device"] = "/dev/ttyUSB0" + controller_node = client.driver.controller.own_node + controller_device_id = ( + f"{client.driver.controller.home_id}-{controller_node.node_id}" + ) + controller_device_id_ext = ( + f"{controller_device_id}-{controller_node.manufacturer_id}:" + f"{controller_node.product_type}:{controller_node.product_id}" + ) + + assert len(device_registry.devices) == 2 + # Verify there's a device entry for the controller. + device = device_registry.async_get_device( + identifiers={(DOMAIN, controller_device_id)} + ) + assert device + assert device == device_registry.async_get_device( + identifiers={(DOMAIN, controller_device_id_ext)} + ) + assert device.manufacturer == "AEON Labs" + assert device.model == "ZW090" + assert device.name == "Z‐Stick Gen5 USB Controller" + # Verify there's a device entry for the multisensor. + sensor_device_id = f"{client.driver.controller.home_id}-{multisensor_6.node_id}" + device = device_registry.async_get_device(identifiers={(DOMAIN, sensor_device_id)}) + assert device + assert device.manufacturer == "AEON Labs" + assert device.model == "ZW100" + assert device.name == "Multisensor 6" + # Customize the sensor device name. + device_registry.async_update_device( + device.id, name_by_user="Custom Sensor Device Name" + ) + async def mock_backup_nvm_raw(): await asyncio.sleep(0) client.driver.controller.emit( @@ -3521,6 +3567,7 @@ async def test_reconfigure_migrate_with_addon( "nvm restore progress", {"event": "nvm restore progress", "bytesWritten": 100, "total": 200}, ) + client.driver.controller.data["homeId"] = 3245146787 client.driver.emit( "driver ready", {"event": "driver ready", "source": "driver"} ) @@ -3591,6 +3638,17 @@ async def test_reconfigure_migrate_with_addon( "core_zwave_js", AddonsOptions(config={"device": "/test"}) ) + # Simulate the new connected controller hardware labels. + # This will cause a new device entry to be created + # when the config entry is loaded before restoring NVM. + controller_node = client.driver.controller.own_node + controller_node.data["manufacturerId"] = 999 + controller_node.data["productId"] = 999 + controller_node.device_config.data["description"] = "New Device Name" + controller_node.device_config.data["label"] = "New Device Model" + controller_node.device_config.data["manufacturer"] = "New Device Manufacturer" + client.driver.controller.data["homeId"] = 5678 + await hass.async_block_till_done() assert restart_addon.call_args == call("core_zwave_js") @@ -3599,14 +3657,14 @@ async def test_reconfigure_migrate_with_addon( assert entry.unique_id == "5678" get_server_version.side_effect = restore_server_version_side_effect - version_info.home_id = 8765 + version_info.home_id = 3245146787 assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "restore_nvm" assert client.connect.call_count == 2 await hass.async_block_till_done() - assert client.connect.call_count == 3 + assert client.connect.call_count == 4 assert entry.state is config_entries.ConfigEntryState.LOADED assert client.driver.controller.async_restore_nvm.call_count == 1 assert len(events) == 2 @@ -3620,8 +3678,29 @@ async def test_reconfigure_migrate_with_addon( assert entry.data["url"] == "ws://host1:3001" assert entry.data["usb_path"] == "/test" assert entry.data["use_addon"] is True + assert "keep_old_devices" not in entry.data assert entry.unique_id == final_unique_id + assert len(device_registry.devices) == 2 + controller_device_id_ext = ( + f"{controller_device_id}-{controller_node.manufacturer_id}:" + f"{controller_node.product_type}:{controller_node.product_id}" + ) + device = device_registry.async_get_device( + identifiers={(DOMAIN, controller_device_id_ext)} + ) + assert device + assert device.manufacturer == "New Device Manufacturer" + assert device.model == "New Device Model" + assert device.name == "New Device Name" + device = device_registry.async_get_device(identifiers={(DOMAIN, sensor_device_id)}) + assert device + assert device.manufacturer == "AEON Labs" + assert device.model == "ZW100" + assert device.name == "Multisensor 6" + assert device.name_by_user == "Custom Sensor Device Name" + assert client.driver.controller.home_id == 3245146787 + @pytest.mark.usefixtures("supervisor", "addon_running") async def test_reconfigure_migrate_reset_driver_ready_timeout( @@ -3755,7 +3834,7 @@ async def test_reconfigure_migrate_reset_driver_ready_timeout( assert client.connect.call_count == 2 await hass.async_block_till_done() - assert client.connect.call_count == 3 + assert client.connect.call_count == 4 assert entry.state is config_entries.ConfigEntryState.LOADED assert client.driver.controller.async_restore_nvm.call_count == 1 assert len(events) == 2 @@ -3770,6 +3849,7 @@ async def test_reconfigure_migrate_reset_driver_ready_timeout( assert entry.data["usb_path"] == "/test" assert entry.data["use_addon"] is True assert entry.unique_id == "5678" + assert "keep_old_devices" not in entry.data @pytest.mark.usefixtures("supervisor", "addon_running") @@ -3895,7 +3975,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 == 3 + assert client.connect.call_count == 4 assert entry.state is config_entries.ConfigEntryState.LOADED assert client.driver.controller.async_restore_nvm.call_count == 1 assert len(events) == 2 @@ -3906,9 +3986,10 @@ async def test_reconfigure_migrate_restore_driver_ready_timeout( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "migration_successful" - assert integration.data["url"] == "ws://host1:3001" - assert integration.data["usb_path"] == "/test" - assert integration.data["use_addon"] is True + assert entry.data["url"] == "ws://host1:3001" + assert entry.data["usb_path"] == "/test" + assert entry.data["use_addon"] is True + assert "keep_old_devices" not in entry.data async def test_reconfigure_migrate_backup_failure( @@ -3942,6 +4023,7 @@ async def test_reconfigure_migrate_backup_failure( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "backup_failed" + assert "keep_old_devices" not in entry.data async def test_reconfigure_migrate_backup_file_failure( @@ -3988,6 +4070,7 @@ async def test_reconfigure_migrate_backup_file_failure( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "backup_failed" + assert "keep_old_devices" not in entry.data @pytest.mark.usefixtures("supervisor", "addon_running") @@ -4073,6 +4156,7 @@ async def test_reconfigure_migrate_start_addon_failure( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "addon_start_failed" + assert "keep_old_devices" not in entry.data @pytest.mark.usefixtures("supervisor", "addon_running", "restart_addon") @@ -4187,6 +4271,7 @@ async def test_reconfigure_migrate_restore_failure( hass.config_entries.flow.async_abort(result["flow_id"]) assert len(hass.config_entries.flow.async_progress()) == 0 + assert "keep_old_devices" not in entry.data async def test_get_driver_failure_intent_migrate( @@ -4196,13 +4281,13 @@ async def test_get_driver_failure_intent_migrate( """Test get driver failure in intent migrate step.""" entry = integration hass.config_entries.async_update_entry( - integration, unique_id="1234", data={**integration.data, "use_addon": True} + entry, unique_id="1234", data={**entry.data, "use_addon": True} ) result = await entry.start_reconfigure_flow(hass) assert result["type"] is FlowResultType.MENU assert result["step_id"] == "reconfigure" - await hass.config_entries.async_unload(integration.entry_id) + await hass.config_entries.async_unload(entry.entry_id) result = await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": "intent_migrate"} @@ -4210,6 +4295,7 @@ async def test_get_driver_failure_intent_migrate( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "config_entry_not_loaded" + assert "keep_old_devices" not in entry.data async def test_get_driver_failure_instruct_unplug( @@ -4231,7 +4317,7 @@ async def test_get_driver_failure_instruct_unplug( ) entry = integration hass.config_entries.async_update_entry( - integration, unique_id="1234", data={**integration.data, "use_addon": True} + entry, unique_id="1234", data={**entry.data, "use_addon": True} ) result = await entry.start_reconfigure_flow(hass) assert result["type"] is FlowResultType.MENU @@ -4254,7 +4340,7 @@ async def test_get_driver_failure_instruct_unplug( assert client.driver.controller.async_backup_nvm_raw.call_count == 1 assert mock_file.call_count == 1 - await hass.config_entries.async_unload(integration.entry_id) + await hass.config_entries.async_unload(entry.entry_id) result = await hass.config_entries.flow.async_configure(result["flow_id"]) @@ -4270,7 +4356,7 @@ async def test_hard_reset_failure( """Test hard reset failure.""" entry = integration hass.config_entries.async_update_entry( - integration, unique_id="1234", data={**integration.data, "use_addon": True} + entry, unique_id="1234", data={**entry.data, "use_addon": True} ) async def mock_backup_nvm_raw(): @@ -4320,7 +4406,7 @@ async def test_choose_serial_port_usb_ports_failure( """Test choose serial port usb ports failure.""" entry = integration hass.config_entries.async_update_entry( - integration, unique_id="1234", data={**integration.data, "use_addon": True} + entry, unique_id="1234", data={**entry.data, "use_addon": True} ) async def mock_backup_nvm_raw(): From d302e817c821bf241e5778cacad1979876274808 Mon Sep 17 00:00:00 2001 From: Ian Date: Tue, 3 Jun 2025 04:20:14 -0700 Subject: [PATCH 078/266] NextBus: Bump py_nextbusnext to 2.2.0 (#145904) --- homeassistant/components/nextbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nextbus/manifest.json b/homeassistant/components/nextbus/manifest.json index a4f6d54f58c..4b7057f7142 100644 --- a/homeassistant/components/nextbus/manifest.json +++ b/homeassistant/components/nextbus/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/nextbus", "iot_class": "cloud_polling", "loggers": ["py_nextbus"], - "requirements": ["py-nextbusnext==2.1.2"] + "requirements": ["py-nextbusnext==2.2.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 63ddad956e9..4408673f575 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1762,7 +1762,7 @@ py-madvr2==1.6.32 py-melissa-climate==2.1.4 # homeassistant.components.nextbus -py-nextbusnext==2.1.2 +py-nextbusnext==2.2.0 # homeassistant.components.nightscout py-nightscout==1.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e3f5788e5d8..6f527a9c393 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1485,7 +1485,7 @@ py-madvr2==1.6.32 py-melissa-climate==2.1.4 # homeassistant.components.nextbus -py-nextbusnext==2.1.2 +py-nextbusnext==2.2.0 # homeassistant.components.nightscout py-nightscout==1.2.2 From 1a21e01f851631ecaff2c4852e644baccadf2c64 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 1 Jun 2025 21:14:19 +0200 Subject: [PATCH 079/266] Bump aioimmich to 0.8.0 (#145908) --- homeassistant/components/immich/manifest.json | 2 +- .../components/immich/media_source.py | 28 +++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/immich/conftest.py | 118 +++++++++++------- tests/components/immich/const.py | 109 ++++++++++++---- .../immich/snapshots/test_diagnostics.ambr | 46 ++++--- .../immich/snapshots/test_sensor.ambr | 4 +- 8 files changed, 206 insertions(+), 105 deletions(-) diff --git a/homeassistant/components/immich/manifest.json b/homeassistant/components/immich/manifest.json index 5b56a7e3e2d..b7c8176356f 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.7.0"] + "requirements": ["aioimmich==0.8.0"] } diff --git a/homeassistant/components/immich/media_source.py b/homeassistant/components/immich/media_source.py index a7c55f9c572..c636fda879a 100644 --- a/homeassistant/components/immich/media_source.py +++ b/homeassistant/components/immich/media_source.py @@ -133,10 +133,10 @@ class ImmichMediaSource(MediaSource): identifier=f"{identifier.unique_id}|albums|{album.album_id}", media_class=MediaClass.DIRECTORY, media_content_type=MediaClass.IMAGE, - title=album.name, + title=album.album_name, can_play=False, can_expand=True, - thumbnail=f"/immich/{identifier.unique_id}/{album.thumbnail_asset_id}/thumbnail/image/jpg", + thumbnail=f"/immich/{identifier.unique_id}/{album.album_thumbnail_asset_id}/thumbnail/image/jpg", ) for album in albums ] @@ -160,18 +160,19 @@ class ImmichMediaSource(MediaSource): f"{identifier.unique_id}|albums|" f"{identifier.collection_id}|" f"{asset.asset_id}|" - f"{asset.file_name}|" - f"{asset.mime_type}" + f"{asset.original_file_name}|" + f"{mime_type}" ), media_class=MediaClass.IMAGE, - media_content_type=asset.mime_type, - title=asset.file_name, + media_content_type=mime_type, + title=asset.original_file_name, can_play=False, can_expand=False, - thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/thumbnail/{asset.mime_type}", + thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/thumbnail/{mime_type}", ) for asset in album_info.assets - if asset.mime_type.startswith("image/") + if (mime_type := asset.original_mime_type) + and mime_type.startswith("image/") ] ret.extend( @@ -181,18 +182,19 @@ class ImmichMediaSource(MediaSource): f"{identifier.unique_id}|albums|" f"{identifier.collection_id}|" f"{asset.asset_id}|" - f"{asset.file_name}|" - f"{asset.mime_type}" + f"{asset.original_file_name}|" + f"{mime_type}" ), media_class=MediaClass.VIDEO, - media_content_type=asset.mime_type, - title=asset.file_name, + media_content_type=mime_type, + title=asset.original_file_name, can_play=True, can_expand=False, thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/thumbnail/image/jpeg", ) for asset in album_info.assets - if asset.mime_type.startswith("video/") + if (mime_type := asset.original_mime_type) + and mime_type.startswith("video/") ) return ret diff --git a/requirements_all.txt b/requirements_all.txt index 4408673f575..a7aeda1007b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -280,7 +280,7 @@ aiohue==4.7.4 aioimaplib==2.0.1 # homeassistant.components.immich -aioimmich==0.7.0 +aioimmich==0.8.0 # homeassistant.components.apache_kafka aiokafka==0.10.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6f527a9c393..10121e31eb9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -265,7 +265,7 @@ aiohue==4.7.4 aioimaplib==2.0.1 # homeassistant.components.immich -aioimmich==0.7.0 +aioimmich==0.8.0 # homeassistant.components.apache_kafka aiokafka==0.10.0 diff --git a/tests/components/immich/conftest.py b/tests/components/immich/conftest.py index 1b9a7df8df7..f8f959e0b0a 100644 --- a/tests/components/immich/conftest.py +++ b/tests/components/immich/conftest.py @@ -1,7 +1,6 @@ """Common fixtures for the Immich tests.""" from collections.abc import AsyncGenerator, Generator -from datetime import datetime from unittest.mock import AsyncMock, patch from aioimmich import ImmichAlbums, ImmichAssests, ImmichServer, ImmichUsers @@ -10,7 +9,7 @@ from aioimmich.server.models import ( ImmichServerStatistics, ImmichServerStorage, ) -from aioimmich.users.models import AvatarColor, ImmichUser, UserStatus +from aioimmich.users.models import ImmichUserObject import pytest from homeassistant.components.immich.const import DOMAIN @@ -78,36 +77,58 @@ def mock_immich_assets() -> AsyncMock: def mock_immich_server() -> AsyncMock: """Mock the Immich server.""" mock = AsyncMock(spec=ImmichServer) - mock.async_get_about_info.return_value = ImmichServerAbout( - "v1.132.3", - "some_url", - False, - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, + mock.async_get_about_info.return_value = ImmichServerAbout.from_dict( + { + "version": "v1.132.3", + "versionUrl": "https://github.com/immich-app/immich/releases/tag/v1.132.3", + "licensed": False, + "build": "14709928600", + "buildUrl": "https://github.com/immich-app/immich/actions/runs/14709928600", + "buildImage": "v1.132.3", + "buildImageUrl": "https://github.com/immich-app/immich/pkgs/container/immich-server", + "repository": "immich-app/immich", + "repositoryUrl": "https://github.com/immich-app/immich", + "sourceRef": "v1.132.3", + "sourceCommit": "02994883fe3f3972323bb6759d0170a4062f5236", + "sourceUrl": "https://github.com/immich-app/immich/commit/02994883fe3f3972323bb6759d0170a4062f5236", + "nodejs": "v22.14.0", + "exiftool": "13.00", + "ffmpeg": "7.0.2-7", + "libvips": "8.16.1", + "imagemagick": "7.1.1-47", + } ) - mock.async_get_storage_info.return_value = ImmichServerStorage( - "294.2 GiB", - "142.9 GiB", - "136.3 GiB", - 315926315008, - 153400434688, - 146402975744, - 48.56, + mock.async_get_storage_info.return_value = ImmichServerStorage.from_dict( + { + "diskSize": "294.2 GiB", + "diskUse": "142.9 GiB", + "diskAvailable": "136.3 GiB", + "diskSizeRaw": 315926315008, + "diskUseRaw": 153400406016, + "diskAvailableRaw": 146403004416, + "diskUsagePercentage": 48.56, + } ) - mock.async_get_server_statistics.return_value = ImmichServerStatistics( - 27038, 1836, 119525451912, 54291170551, 65234281361 + mock.async_get_server_statistics.return_value = ImmichServerStatistics.from_dict( + { + "photos": 27038, + "videos": 1836, + "usage": 119525451912, + "usagePhotos": 54291170551, + "usageVideos": 65234281361, + "usageByUser": [ + { + "userId": "e7ef5713-9dab-4bd4-b899-715b0ca4379e", + "userName": "admin", + "photos": 27038, + "videos": 1836, + "usage": 119525451912, + "usagePhotos": 54291170551, + "usageVideos": 65234281361, + "quotaSizeInBytes": None, + } + ], + } ) return mock @@ -116,23 +137,26 @@ def mock_immich_server() -> AsyncMock: def mock_immich_user() -> AsyncMock: """Mock the Immich server.""" mock = AsyncMock(spec=ImmichUsers) - mock.async_get_my_user.return_value = ImmichUser( - "e7ef5713-9dab-4bd4-b899-715b0ca4379e", - "user@immich.local", - "user", - "", - AvatarColor.PRIMARY, - datetime.fromisoformat("2025-05-11T10:07:46.866Z"), - "user", - False, - True, - datetime.fromisoformat("2025-05-11T10:07:46.866Z"), - None, - None, - "", - None, - None, - UserStatus.ACTIVE, + mock.async_get_my_user.return_value = ImmichUserObject.from_dict( + { + "id": "e7ef5713-9dab-4bd4-b899-715b0ca4379e", + "email": "user@immich.local", + "name": "user", + "profileImagePath": "", + "avatarColor": "primary", + "profileChangedAt": "2025-05-11T10:07:46.866Z", + "storageLabel": "user", + "shouldChangePassword": True, + "isAdmin": True, + "createdAt": "2025-05-11T10:07:46.866Z", + "deletedAt": None, + "updatedAt": "2025-05-18T00:59:55.547Z", + "oauthId": "", + "quotaSizeInBytes": None, + "quotaUsageInBytes": 119526467534, + "status": "active", + "license": None, + } ) return mock diff --git a/tests/components/immich/const.py b/tests/components/immich/const.py index ac0b221f721..97721bc7dbc 100644 --- a/tests/components/immich/const.py +++ b/tests/components/immich/const.py @@ -1,7 +1,6 @@ """Constants for the Immich integration tests.""" from aioimmich.albums.models import ImmichAlbum -from aioimmich.assets.models import ImmichAsset from homeassistant.const import ( CONF_API_KEY, @@ -26,27 +25,91 @@ MOCK_CONFIG_ENTRY_DATA = { CONF_VERIFY_SSL: False, } -MOCK_ALBUM_WITHOUT_ASSETS = ImmichAlbum( - "721e1a4b-aa12-441e-8d3b-5ac7ab283bb6", - "My Album", - "This is my first great album", - "0d03a7ad-ddc7-45a6-adee-68d322a6d2f5", - 1, - [], -) +ALBUM_DATA = { + "id": "721e1a4b-aa12-441e-8d3b-5ac7ab283bb6", + "albumName": "My Album", + "albumThumbnailAssetId": "0d03a7ad-ddc7-45a6-adee-68d322a6d2f5", + "albumUsers": [], + "assetCount": 1, + "assets": [], + "createdAt": "2025-05-11T10:13:22.799Z", + "hasSharedLink": False, + "isActivityEnabled": False, + "ownerId": "e7ef5713-9dab-4bd4-b899-715b0ca4379e", + "owner": { + "id": "e7ef5713-9dab-4bd4-b899-715b0ca4379e", + "email": "admin@immich.local", + "name": "admin", + "profileImagePath": "", + "avatarColor": "primary", + "profileChangedAt": "2025-05-11T10:07:46.866Z", + }, + "shared": False, + "updatedAt": "2025-05-17T11:26:03.696Z", +} -MOCK_ALBUM_WITH_ASSETS = ImmichAlbum( - "721e1a4b-aa12-441e-8d3b-5ac7ab283bb6", - "My Album", - "This is my first great album", - "0d03a7ad-ddc7-45a6-adee-68d322a6d2f5", - 1, - [ - ImmichAsset( - "2e94c203-50aa-4ad2-8e29-56dd74e0eff4", "filename.jpg", "image/jpeg" - ), - ImmichAsset( - "2e65a5f2-db83-44c4-81ab-f5ff20c9bd7b", "filename.mp4", "video/mp4" - ), - ], +MOCK_ALBUM_WITHOUT_ASSETS = ImmichAlbum.from_dict(ALBUM_DATA) + +MOCK_ALBUM_WITH_ASSETS = ImmichAlbum.from_dict( + { + **ALBUM_DATA, + "assets": [ + { + "id": "2e94c203-50aa-4ad2-8e29-56dd74e0eff4", + "deviceAssetId": "web-filename.jpg-1675185639000", + "ownerId": "e7ef5713-9dab-4bd4-b899-715b0ca4379e", + "deviceId": "WEB", + "libraryId": None, + "type": "IMAGE", + "originalPath": "upload/upload/e7ef5713-9dab-4bd4-b899-715b0ca4379e/b4/b8/b4b8ef00-8a6d-4056-91ff-7f86dc66e427.jpg", + "originalFileName": "filename.jpg", + "originalMimeType": "image/jpeg", + "thumbhash": "1igGFALX8mVGdHc5aChJf5nxNg==", + "fileCreatedAt": "2023-01-31T17:20:37.085+00:00", + "fileModifiedAt": "2023-01-31T17:20:39+00:00", + "localDateTime": "2023-01-31T18:20:37.085+00:00", + "updatedAt": "2025-05-11T10:13:49.590401+00:00", + "isFavorite": False, + "isArchived": False, + "isTrashed": False, + "duration": "0:00:00.00000", + "exifInfo": {}, + "livePhotoVideoId": None, + "people": [], + "checksum": "HJm7TVOP80S+eiYZnAhWyRaB/Yc=", + "isOffline": False, + "hasMetadata": True, + "duplicateId": None, + "resized": True, + }, + { + "id": "2e65a5f2-db83-44c4-81ab-f5ff20c9bd7b", + "deviceAssetId": "web-filename.mp4-1675185639000", + "ownerId": "e7ef5713-9dab-4bd4-b899-715b0ca4379e", + "deviceId": "WEB", + "libraryId": None, + "type": "IMAGE", + "originalPath": "upload/upload/e7ef5713-9dab-4bd4-b899-715b0ca4379e/b4/b8/b4b8ef00-8a6d-4056-eeff-7f86dc66e427.mp4", + "originalFileName": "filename.mp4", + "originalMimeType": "video/mp4", + "thumbhash": "1igGFALX8mVGdHc5aChJf5nxNg==", + "fileCreatedAt": "2023-01-31T17:20:37.085+00:00", + "fileModifiedAt": "2023-01-31T17:20:39+00:00", + "localDateTime": "2023-01-31T18:20:37.085+00:00", + "updatedAt": "2025-05-11T10:13:49.590401+00:00", + "isFavorite": False, + "isArchived": False, + "isTrashed": False, + "duration": "0:00:00.00000", + "exifInfo": {}, + "livePhotoVideoId": None, + "people": [], + "checksum": "HJm7TVOP80S+eiYZnAhWyRaB/Yc=", + "isOffline": False, + "hasMetadata": True, + "duplicateId": None, + "resized": True, + }, + ], + } ) diff --git a/tests/components/immich/snapshots/test_diagnostics.ambr b/tests/components/immich/snapshots/test_diagnostics.ambr index 3216de2fabd..b3dd3c47db6 100644 --- a/tests/components/immich/snapshots/test_diagnostics.ambr +++ b/tests/components/immich/snapshots/test_diagnostics.ambr @@ -3,36 +3,48 @@ dict({ 'data': dict({ 'server_about': dict({ - 'build': None, - 'build_image': None, - 'build_image_url': None, - 'build_url': None, - 'exiftool': None, - 'ffmpeg': None, - 'imagemagick': None, - 'libvips': None, + 'build': '14709928600', + 'build_image': 'v1.132.3', + 'build_image_url': 'https://github.com/immich-app/immich/pkgs/container/immich-server', + 'build_url': 'https://github.com/immich-app/immich/actions/runs/14709928600', + 'exiftool': '13.00', + 'ffmpeg': '7.0.2-7', + 'imagemagick': '7.1.1-47', + 'libvips': '8.16.1', 'licensed': False, - 'nodejs': None, - 'repository': None, - 'repository_url': None, - 'source_commit': None, - 'source_ref': None, - 'source_url': None, + 'nodejs': 'v22.14.0', + 'repository': 'immich-app/immich', + 'repository_url': 'https://github.com/immich-app/immich', + 'source_commit': '02994883fe3f3972323bb6759d0170a4062f5236', + 'source_ref': 'v1.132.3', + 'source_url': 'https://github.com/immich-app/immich/commit/02994883fe3f3972323bb6759d0170a4062f5236', 'version': 'v1.132.3', - 'version_url': 'some_url', + 'version_url': 'https://github.com/immich-app/immich/releases/tag/v1.132.3', }), 'server_storage': dict({ 'disk_available': '136.3 GiB', - 'disk_available_raw': 146402975744, + 'disk_available_raw': 146403004416, 'disk_size': '294.2 GiB', 'disk_size_raw': 315926315008, 'disk_usage_percentage': 48.56, 'disk_use': '142.9 GiB', - 'disk_use_raw': 153400434688, + 'disk_use_raw': 153400406016, }), 'server_usage': dict({ 'photos': 27038, 'usage': 119525451912, + 'usage_by_user': list([ + dict({ + 'photos': 27038, + 'quota_size_in_bytes': None, + 'usage': 119525451912, + 'usage_photos': 54291170551, + 'usage_videos': 65234281361, + 'user_id': 'e7ef5713-9dab-4bd4-b899-715b0ca4379e', + 'user_name': 'admin', + 'videos': 1836, + }), + ]), 'usage_photos': 54291170551, 'usage_videos': 65234281361, 'videos': 1836, diff --git a/tests/components/immich/snapshots/test_sensor.ambr b/tests/components/immich/snapshots/test_sensor.ambr index d1ae9a8be8d..590e7d9ad5c 100644 --- a/tests/components/immich/snapshots/test_sensor.ambr +++ b/tests/components/immich/snapshots/test_sensor.ambr @@ -55,7 +55,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '136.34839630127', + 'state': '136.34842300415', }) # --- # name: test_sensors[sensor.someone_disk_size-entry] @@ -225,7 +225,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '142.865287780762', + 'state': '142.865261077881', }) # --- # name: test_sensors[sensor.someone_disk_used_by_photos-entry] From 88f2c3abd3909f23d00c168de622c805997f1da5 Mon Sep 17 00:00:00 2001 From: TimL Date: Sun, 1 Jun 2025 11:14:08 +1000 Subject: [PATCH 080/266] Bump pysmlight to v0.2.5 (#145949) --- 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 b2a03a737fc..f47960a65bd 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.4"], + "requirements": ["pysmlight==0.2.5"], "zeroconf": [ { "type": "_slzb-06._tcp.local." diff --git a/requirements_all.txt b/requirements_all.txt index a7aeda1007b..bcb2237124b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2353,7 +2353,7 @@ pysmhi==1.0.2 pysml==0.0.12 # homeassistant.components.smlight -pysmlight==0.2.4 +pysmlight==0.2.5 # homeassistant.components.snmp pysnmp==6.2.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 10121e31eb9..cb0e7035770 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1953,7 +1953,7 @@ pysmhi==1.0.2 pysml==0.0.12 # homeassistant.components.smlight -pysmlight==0.2.4 +pysmlight==0.2.5 # homeassistant.components.snmp pysnmp==6.2.6 From 7e851370126fcf64fd47a66ecf4cf6b08e7a0abd Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 2 Jun 2025 00:47:05 -0700 Subject: [PATCH 081/266] Bump ical to 10.0.0 (#145954) --- homeassistant/components/google/manifest.json | 2 +- homeassistant/components/local_calendar/manifest.json | 2 +- homeassistant/components/local_todo/manifest.json | 2 +- homeassistant/components/remote_calendar/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index c5a9d4784bc..fecd245869a 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/google", "iot_class": "cloud_polling", "loggers": ["googleapiclient"], - "requirements": ["gcal-sync==7.1.0", "oauth2client==4.1.3", "ical==9.2.5"] + "requirements": ["gcal-sync==7.1.0", "oauth2client==4.1.3", "ical==10.0.0"] } diff --git a/homeassistant/components/local_calendar/manifest.json b/homeassistant/components/local_calendar/manifest.json index fc636d75482..e0b08313d63 100644 --- a/homeassistant/components/local_calendar/manifest.json +++ b/homeassistant/components/local_calendar/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/local_calendar", "iot_class": "local_polling", "loggers": ["ical"], - "requirements": ["ical==9.2.5"] + "requirements": ["ical==10.0.0"] } diff --git a/homeassistant/components/local_todo/manifest.json b/homeassistant/components/local_todo/manifest.json index cd19090f400..c8e80e4f91b 100644 --- a/homeassistant/components/local_todo/manifest.json +++ b/homeassistant/components/local_todo/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/local_todo", "iot_class": "local_polling", - "requirements": ["ical==9.2.5"] + "requirements": ["ical==10.0.0"] } diff --git a/homeassistant/components/remote_calendar/manifest.json b/homeassistant/components/remote_calendar/manifest.json index 60b5e15e8fb..7bdc5362ae7 100644 --- a/homeassistant/components/remote_calendar/manifest.json +++ b/homeassistant/components/remote_calendar/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["ical"], "quality_scale": "silver", - "requirements": ["ical==9.2.5"] + "requirements": ["ical==10.0.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index bcb2237124b..68241b5d5ff 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1203,7 +1203,7 @@ ibmiotf==0.3.4 # homeassistant.components.local_calendar # homeassistant.components.local_todo # homeassistant.components.remote_calendar -ical==9.2.5 +ical==10.0.0 # homeassistant.components.caldav icalendar==6.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cb0e7035770..e8176cbd41f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1040,7 +1040,7 @@ ibeacon-ble==1.2.0 # homeassistant.components.local_calendar # homeassistant.components.local_todo # homeassistant.components.remote_calendar -ical==9.2.5 +ical==10.0.0 # homeassistant.components.caldav icalendar==6.1.0 From f280032dcf8deffcc152359721e82b7c97b5999b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noah=20Gro=C3=9F?= Date: Tue, 3 Jun 2025 10:57:59 +0200 Subject: [PATCH 082/266] Bump python-picnic-api2 to 1.3.1 (#145962) --- homeassistant/components/picnic/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/picnic/manifest.json b/homeassistant/components/picnic/manifest.json index 251964c15d0..e7623c5eb03 100644 --- a/homeassistant/components/picnic/manifest.json +++ b/homeassistant/components/picnic/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/picnic", "iot_class": "cloud_polling", "loggers": ["python_picnic_api2"], - "requirements": ["python-picnic-api2==1.2.4"] + "requirements": ["python-picnic-api2==1.3.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 68241b5d5ff..ca3ef670783 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2486,7 +2486,7 @@ python-otbr-api==2.7.0 python-overseerr==0.7.1 # homeassistant.components.picnic -python-picnic-api2==1.2.4 +python-picnic-api2==1.3.1 # homeassistant.components.rabbitair python-rabbitair==0.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e8176cbd41f..297e3ff5958 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2056,7 +2056,7 @@ python-otbr-api==2.7.0 python-overseerr==0.7.1 # homeassistant.components.picnic -python-picnic-api2==1.2.4 +python-picnic-api2==1.3.1 # homeassistant.components.rabbitair python-rabbitair==0.0.8 From d729eed7c2f3b31a557601ea2aca38e7de368504 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 2 Jun 2025 10:48:42 +0300 Subject: [PATCH 083/266] Add diagnostics to Amazon devices (#145964) --- .../components/amazon_devices/diagnostics.py | 66 +++++++++++++++++ .../snapshots/test_diagnostics.ambr | 74 +++++++++++++++++++ .../amazon_devices/test_diagnostics.py | 70 ++++++++++++++++++ 3 files changed, 210 insertions(+) create mode 100644 homeassistant/components/amazon_devices/diagnostics.py create mode 100644 tests/components/amazon_devices/snapshots/test_diagnostics.ambr create mode 100644 tests/components/amazon_devices/test_diagnostics.py diff --git a/homeassistant/components/amazon_devices/diagnostics.py b/homeassistant/components/amazon_devices/diagnostics.py new file mode 100644 index 00000000000..e9a0773cd3f --- /dev/null +++ b/homeassistant/components/amazon_devices/diagnostics.py @@ -0,0 +1,66 @@ +"""Diagnostics support for Amazon Devices integration.""" + +from __future__ import annotations + +from typing import Any + +from aioamazondevices.api import AmazonDevice + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntry + +from .coordinator import AmazonConfigEntry + +TO_REDACT = {CONF_PASSWORD, CONF_USERNAME, CONF_NAME, "title"} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: AmazonConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + + coordinator = entry.runtime_data + + devices: list[dict[str, dict[str, Any]]] = [ + build_device_data(device) for device in coordinator.data.values() + ] + + return { + "entry": async_redact_data(entry.as_dict(), TO_REDACT), + "device_info": { + "last_update success": coordinator.last_update_success, + "last_exception": repr(coordinator.last_exception), + "devices": devices, + }, + } + + +async def async_get_device_diagnostics( + hass: HomeAssistant, entry: AmazonConfigEntry, device_entry: DeviceEntry +) -> dict[str, Any]: + """Return diagnostics for a device.""" + + coordinator = entry.runtime_data + + assert device_entry.serial_number + + return build_device_data(coordinator.data[device_entry.serial_number]) + + +def build_device_data(device: AmazonDevice) -> dict[str, Any]: + """Build device data for diagnostics.""" + return { + "account name": device.account_name, + "capabilities": device.capabilities, + "device family": device.device_family, + "device type": device.device_type, + "device cluster members": device.device_cluster_members, + "online": device.online, + "serial number": device.serial_number, + "software version": device.software_version, + "do not disturb": device.do_not_disturb, + "response style": device.response_style, + "bluetooth state": device.bluetooth_state, + } diff --git a/tests/components/amazon_devices/snapshots/test_diagnostics.ambr b/tests/components/amazon_devices/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..0b5164418aa --- /dev/null +++ b/tests/components/amazon_devices/snapshots/test_diagnostics.ambr @@ -0,0 +1,74 @@ +# serializer version: 1 +# name: test_device_diagnostics + dict({ + 'account name': 'Echo Test', + 'bluetooth state': True, + 'capabilities': list([ + 'AUDIO_PLAYER', + 'MICROPHONE', + ]), + 'device cluster members': list([ + 'echo_test_serial_number', + ]), + 'device family': 'mine', + 'device type': 'echo', + 'do not disturb': False, + 'online': True, + 'response style': None, + 'serial number': 'echo_test_serial_number', + 'software version': 'echo_test_software_version', + }) +# --- +# name: test_entry_diagnostics + dict({ + 'device_info': dict({ + 'devices': list([ + dict({ + 'account name': 'Echo Test', + 'bluetooth state': True, + 'capabilities': list([ + 'AUDIO_PLAYER', + 'MICROPHONE', + ]), + 'device cluster members': list([ + 'echo_test_serial_number', + ]), + 'device family': 'mine', + 'device type': 'echo', + 'do not disturb': False, + 'online': True, + 'response style': None, + 'serial number': 'echo_test_serial_number', + 'software version': 'echo_test_software_version', + }), + ]), + 'last_exception': 'None', + 'last_update success': True, + }), + 'entry': dict({ + 'data': dict({ + 'country': 'IT', + 'login_data': dict({ + 'session': 'test-session', + }), + 'password': '**REDACTED**', + 'username': '**REDACTED**', + }), + 'disabled_by': None, + 'discovery_keys': dict({ + }), + 'domain': 'amazon_devices', + 'minor_version': 1, + 'options': dict({ + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'subentries': list([ + ]), + 'title': '**REDACTED**', + 'unique_id': 'fake_email@gmail.com', + 'version': 1, + }), + }) +# --- diff --git a/tests/components/amazon_devices/test_diagnostics.py b/tests/components/amazon_devices/test_diagnostics.py new file mode 100644 index 00000000000..e548702650b --- /dev/null +++ b/tests/components/amazon_devices/test_diagnostics.py @@ -0,0 +1,70 @@ +"""Tests for Amazon Devices diagnostics platform.""" + +from __future__ import annotations + +from unittest.mock import AsyncMock + +from syrupy.assertion import SnapshotAssertion +from syrupy.filters import props + +from homeassistant.components.amazon_devices.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from . import setup_integration +from .const import TEST_SERIAL_NUMBER + +from tests.common import MockConfigEntry +from tests.components.diagnostics import ( + get_diagnostics_for_config_entry, + get_diagnostics_for_device, +) +from tests.typing import ClientSessionGenerator + + +async def test_entry_diagnostics( + hass: HomeAssistant, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, + hass_client: ClientSessionGenerator, + snapshot: SnapshotAssertion, +) -> None: + """Test Amazon config entry diagnostics.""" + await setup_integration(hass, mock_config_entry) + + assert await get_diagnostics_for_config_entry( + hass, hass_client, mock_config_entry + ) == snapshot( + exclude=props( + "entry_id", + "created_at", + "modified_at", + ) + ) + + +async def test_device_diagnostics( + hass: HomeAssistant, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, + hass_client: ClientSessionGenerator, + device_registry: dr.DeviceRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test Amazon device diagnostics.""" + await setup_integration(hass, mock_config_entry) + + device = device_registry.async_get_device( + identifiers={(DOMAIN, TEST_SERIAL_NUMBER)} + ) + assert device, repr(device_registry.devices) + + assert await get_diagnostics_for_device( + hass, hass_client, mock_config_entry, device + ) == snapshot( + exclude=props( + "entry_id", + "created_at", + "modified_at", + ) + ) From 6defed2915ce53f08d3b111608fb6d8ff86071b9 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sun, 1 Jun 2025 22:15:18 +0300 Subject: [PATCH 084/266] Bump aioamazondevices to 3.0.4 (#145971) --- homeassistant/components/amazon_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/amazon_devices/manifest.json b/homeassistant/components/amazon_devices/manifest.json index eb9fae6ddbe..a24671298d9 100644 --- a/homeassistant/components/amazon_devices/manifest.json +++ b/homeassistant/components/amazon_devices/manifest.json @@ -118,5 +118,5 @@ "iot_class": "cloud_polling", "loggers": ["aioamazondevices"], "quality_scale": "bronze", - "requirements": ["aioamazondevices==2.1.1"] + "requirements": ["aioamazondevices==3.0.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index ca3ef670783..a5eada9ccc2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -182,7 +182,7 @@ aioairzone-cloud==0.6.12 aioairzone==1.0.0 # homeassistant.components.amazon_devices -aioamazondevices==2.1.1 +aioamazondevices==3.0.4 # homeassistant.components.ambient_network # homeassistant.components.ambient_station diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 297e3ff5958..3611eaac1f7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -170,7 +170,7 @@ aioairzone-cloud==0.6.12 aioairzone==1.0.0 # homeassistant.components.amazon_devices -aioamazondevices==2.1.1 +aioamazondevices==3.0.4 # homeassistant.components.ambient_network # homeassistant.components.ambient_station From 9e1d8c2fc6f58d8d909356ec17be3c8913828981 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 2 Jun 2025 15:11:56 +0200 Subject: [PATCH 085/266] Bump reolink-aio to 0.13.5 (#145974) * Add debug logging * Bump reolink-aio to 0.13.5 * Revert "Add debug logging" This reverts commit f96030a6c8dccca7888b6d1274d5ed3a251ac03c. --- 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 694dd43a532..5ae8b0305e4 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -19,5 +19,5 @@ "iot_class": "local_push", "loggers": ["reolink_aio"], "quality_scale": "platinum", - "requirements": ["reolink-aio==0.13.4"] + "requirements": ["reolink-aio==0.13.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index a5eada9ccc2..28188b36675 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2652,7 +2652,7 @@ renault-api==0.3.1 renson-endura-delta==1.7.2 # homeassistant.components.reolink -reolink-aio==0.13.4 +reolink-aio==0.13.5 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3611eaac1f7..9c895522d12 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2195,7 +2195,7 @@ renault-api==0.3.1 renson-endura-delta==1.7.2 # homeassistant.components.reolink -reolink-aio==0.13.4 +reolink-aio==0.13.5 # homeassistant.components.rflink rflink==0.0.66 From 76269333528940ccae6a37c41a57f781578cb59a Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Mon, 2 Jun 2025 20:48:40 +0200 Subject: [PATCH 086/266] Bump go2rtc-client to 0.2.1 (#146019) * Bump go2rtc-client to 0.2.0 * Bump go2rtc-client to 0.2.1 * Clean up hassfest exception --------- Co-authored-by: Martin Hjelmare --- homeassistant/components/go2rtc/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test.txt | 2 +- requirements_test_all.txt | 2 +- script/hassfest/docker/Dockerfile | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/go2rtc/manifest.json b/homeassistant/components/go2rtc/manifest.json index 09f7b3fd74c..dd50b4ba076 100644 --- a/homeassistant/components/go2rtc/manifest.json +++ b/homeassistant/components/go2rtc/manifest.json @@ -8,6 +8,6 @@ "integration_type": "system", "iot_class": "local_polling", "quality_scale": "internal", - "requirements": ["go2rtc-client==0.1.3b0"], + "requirements": ["go2rtc-client==0.2.1"], "single_config_entry": true } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5aa0d8ae82b..21b9d5e064a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ cronsim==2.6 cryptography==45.0.1 dbus-fast==2.43.0 fnv-hash-fast==1.5.0 -go2rtc-client==0.1.3b0 +go2rtc-client==0.2.1 ha-ffmpeg==3.2.2 habluetooth==3.48.2 hass-nabucasa==0.101.0 diff --git a/requirements_all.txt b/requirements_all.txt index 28188b36675..4af2e16aa4a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1026,7 +1026,7 @@ gitterpy==0.1.7 glances-api==0.8.0 # homeassistant.components.go2rtc -go2rtc-client==0.1.3b0 +go2rtc-client==0.2.1 # homeassistant.components.goalzero goalzero==0.2.2 diff --git a/requirements_test.txt b/requirements_test.txt index 40349402c4d..e97b71fa7dc 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -10,7 +10,7 @@ astroid==3.3.10 coverage==7.6.12 freezegun==1.5.1 -go2rtc-client==0.1.3b0 +go2rtc-client==0.2.1 license-expression==30.4.1 mock-open==1.4.0 mypy-dev==1.16.0a8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9c895522d12..2f51d93402b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -890,7 +890,7 @@ gios==6.0.0 glances-api==0.8.0 # homeassistant.components.go2rtc -go2rtc-client==0.1.3b0 +go2rtc-client==0.2.1 # homeassistant.components.goalzero goalzero==0.2.2 diff --git a/script/hassfest/docker/Dockerfile b/script/hassfest/docker/Dockerfile index 647755d8237..981dc3344c0 100644 --- a/script/hassfest/docker/Dockerfile +++ b/script/hassfest/docker/Dockerfile @@ -25,7 +25,7 @@ RUN --mount=from=ghcr.io/astral-sh/uv:0.7.1,source=/uv,target=/bin/uv \ -c /usr/src/homeassistant/homeassistant/package_constraints.txt \ -r /usr/src/homeassistant/requirements.txt \ stdlib-list==0.10.0 pipdeptree==2.26.1 tqdm==4.67.1 ruff==0.11.0 \ - PyTurboJPEG==1.7.5 go2rtc-client==0.1.3b0 ha-ffmpeg==3.2.2 hassil==2.2.3 home-assistant-intents==2025.5.28 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2 + PyTurboJPEG==1.7.5 go2rtc-client==0.2.1 ha-ffmpeg==3.2.2 hassil==2.2.3 home-assistant-intents==2025.5.28 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2 LABEL "name"="hassfest" LABEL "maintainer"="Home Assistant " From 6f0947419359cec70eb09e86d988cd7ed6ea7f29 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Jun 2025 14:04:02 +0100 Subject: [PATCH 087/266] Bump grpcio to 1.72.1 (#146029) --- homeassistant/package_constraints.txt | 6 +++--- script/gen_requirements_all.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 21b9d5e064a..598e2834e86 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -88,9 +88,9 @@ httplib2>=0.19.0 # gRPC is an implicit dependency that we want to make explicit so we manage # upgrades intentionally. It is a large package to build from source and we # want to ensure we have wheels built. -grpcio==1.72.0 -grpcio-status==1.72.0 -grpcio-reflection==1.72.0 +grpcio==1.72.1 +grpcio-status==1.72.1 +grpcio-reflection==1.72.1 # This is a old unmaintained library and is replaced with pycryptodome pycrypto==1000000000.0.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 082062c53a0..606141e4c65 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -115,9 +115,9 @@ httplib2>=0.19.0 # gRPC is an implicit dependency that we want to make explicit so we manage # upgrades intentionally. It is a large package to build from source and we # want to ensure we have wheels built. -grpcio==1.72.0 -grpcio-status==1.72.0 -grpcio-reflection==1.72.0 +grpcio==1.72.1 +grpcio-status==1.72.1 +grpcio-reflection==1.72.1 # This is a old unmaintained library and is replaced with pycryptodome pycrypto==1000000000.0.0 From cf521d4c7c6f17014ddcc7e203eb95d7322dea25 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 2 Jun 2025 15:12:13 +0200 Subject: [PATCH 088/266] Improve debug logging Reolink (#146033) Add debug logging --- homeassistant/components/reolink/__init__.py | 4 ++++ homeassistant/components/reolink/config_flow.py | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index 57d41c20521..38f8e709b5c 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -150,6 +150,10 @@ async def async_setup_entry( if host.api.new_devices and config_entry.state == ConfigEntryState.LOADED: # Their are new cameras/chimes connected, reload to add them. + _LOGGER.debug( + "Reloading Reolink %s to add new device (capabilities)", + host.api.nvr_name, + ) hass.async_create_task( hass.config_entries.async_reload(config_entry.entry_id) ) diff --git a/homeassistant/components/reolink/config_flow.py b/homeassistant/components/reolink/config_flow.py index 12ccd455be3..659169c3618 100644 --- a/homeassistant/components/reolink/config_flow.py +++ b/homeassistant/components/reolink/config_flow.py @@ -194,6 +194,13 @@ class ReolinkFlowHandler(ConfigFlow, domain=DOMAIN): ) raise AbortFlow("already_configured") + if existing_entry and existing_entry.data[CONF_HOST] != discovery_info.ip: + _LOGGER.debug( + "Reolink DHCP reported new IP '%s', updating from old IP '%s'", + discovery_info.ip, + existing_entry.data[CONF_HOST], + ) + self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.ip}) self.context["title_placeholders"] = { From e5cb77d1681bbe8afa545d4c3f7566cbd97b8bb3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 3 Jun 2025 12:05:33 +0200 Subject: [PATCH 089/266] Adjust ConnectionFailure logging in SamsungTV (#146044) --- homeassistant/components/samsungtv/bridge.py | 21 +++++++---- .../components/samsungtv/test_media_player.py | 35 +++++++++++++++++-- 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index 11da83219c7..d8682856752 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -636,14 +636,21 @@ class SamsungTVWSBridge( ) self._remote = None except ConnectionFailure as err: - LOGGER.warning( - ( + error_details = err.args[0] + if "ms.channel.timeOut" in (error_details := repr(err)): + # The websocket was connected, but the TV is probably asleep + LOGGER.debug( + "Channel timeout occurred trying to get remote for %s: %s", + self.host, + error_details, + ) + else: + LOGGER.warning( "Unexpected ConnectionFailure trying to get remote for %s, " - "please report this issue: %s" - ), - self.host, - repr(err), - ) + "please report this issue: %s", + self.host, + error_details, + ) self._remote = None except (WebSocketException, AsyncioTimeoutError, OSError) as err: LOGGER.debug("Failed to get remote for %s: %s", self.host, repr(err)) diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 58797b67423..1bf3c953fc6 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -409,7 +409,7 @@ async def test_update_ws_connection_failure( patch.object( remote_websocket, "start_listening", - side_effect=ConnectionFailure('{"event": "ms.voiceApp.hide"}'), + side_effect=ConnectionFailure({"event": "ms.voiceApp.hide"}), ), patch.object(remote_websocket, "is_alive", return_value=False), ): @@ -419,7 +419,7 @@ async def test_update_ws_connection_failure( assert ( "Unexpected ConnectionFailure trying to get remote for fake_host, please " - 'report this issue: ConnectionFailure(\'{"event": "ms.voiceApp.hide"}\')' + "report this issue: ConnectionFailure({'event': 'ms.voiceApp.hide'})" in caplog.text ) @@ -427,6 +427,37 @@ async def test_update_ws_connection_failure( assert state.state == STATE_OFF +@pytest.mark.usefixtures("rest_api") +async def test_update_ws_connection_failure_channel_timeout( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + remote_websocket: Mock, + caplog: pytest.LogCaptureFixture, +) -> None: + """Testing update tv connection failure exception.""" + await setup_samsungtv_entry(hass, MOCK_CONFIGWS) + + with ( + patch.object( + remote_websocket, + "start_listening", + side_effect=ConnectionFailure({"event": "ms.channel.timeOut"}), + ), + patch.object(remote_websocket, "is_alive", return_value=False), + ): + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + assert ( + "Channel timeout occurred trying to get remote for fake_host: " + "ConnectionFailure({'event': 'ms.channel.timeOut'})" in caplog.text + ) + + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_OFF + + @pytest.mark.usefixtures("rest_api") async def test_update_ws_connection_closed( hass: HomeAssistant, freezer: FrozenDateTimeFactory, remote_websocket: Mock From e15edbd54b3350df4b5ebc483d2a06e5c66d980a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 3 Jun 2025 12:30:18 +0200 Subject: [PATCH 090/266] Adjust SamsungTV on/off logging (#146045) * Adjust SamsungTV on/off logging * Update coordinator.py --- homeassistant/components/samsungtv/coordinator.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/samsungtv/coordinator.py b/homeassistant/components/samsungtv/coordinator.py index ed3c24946ab..9b09436be88 100644 --- a/homeassistant/components/samsungtv/coordinator.py +++ b/homeassistant/components/samsungtv/coordinator.py @@ -39,7 +39,7 @@ class SamsungTVDataUpdateCoordinator(DataUpdateCoordinator[None]): ) self.bridge = bridge - self.is_on: bool | None = False + self.is_on: bool | None = None self.async_extra_update: Callable[[], Coroutine[Any, Any, None]] | None = None async def _async_update_data(self) -> None: @@ -52,7 +52,12 @@ class SamsungTVDataUpdateCoordinator(DataUpdateCoordinator[None]): else: self.is_on = await self.bridge.async_is_on() if self.is_on != old_state: - LOGGER.debug("TV %s state updated to %s", self.bridge.host, self.is_on) + LOGGER.debug( + "TV %s state updated from %s to %s", + self.bridge.host, + old_state, + self.is_on, + ) if self.async_extra_update: await self.async_extra_update() From 999c9b3dc53b625b5c2521ba8c0637fccc1da1bd Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 2 Jun 2025 19:58:54 +0200 Subject: [PATCH 091/266] Don't use multi-line conditionals in immich (#146062) --- .../components/immich/media_source.py | 73 ++++++++----------- 1 file changed, 32 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/immich/media_source.py b/homeassistant/components/immich/media_source.py index c636fda879a..caf8264895b 100644 --- a/homeassistant/components/immich/media_source.py +++ b/homeassistant/components/immich/media_source.py @@ -153,49 +153,40 @@ class ImmichMediaSource(MediaSource): except ImmichError: return [] - ret = [ - BrowseMediaSource( - domain=DOMAIN, - identifier=( - f"{identifier.unique_id}|albums|" - f"{identifier.collection_id}|" - f"{asset.asset_id}|" - f"{asset.original_file_name}|" - f"{mime_type}" - ), - media_class=MediaClass.IMAGE, - media_content_type=mime_type, - title=asset.original_file_name, - can_play=False, - can_expand=False, - thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/thumbnail/{mime_type}", - ) - for asset in album_info.assets - if (mime_type := asset.original_mime_type) - and mime_type.startswith("image/") - ] + ret: list[BrowseMediaSource] = [] + for asset in album_info.assets: + if not (mime_type := asset.original_mime_type) or not mime_type.startswith( + ("image/", "video/") + ): + continue - ret.extend( - BrowseMediaSource( - domain=DOMAIN, - identifier=( - f"{identifier.unique_id}|albums|" - f"{identifier.collection_id}|" - f"{asset.asset_id}|" - f"{asset.original_file_name}|" - f"{mime_type}" - ), - media_class=MediaClass.VIDEO, - media_content_type=mime_type, - title=asset.original_file_name, - can_play=True, - can_expand=False, - thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/thumbnail/image/jpeg", + if mime_type.startswith("image/"): + media_class = MediaClass.IMAGE + can_play = False + thumb_mime_type = mime_type + else: + media_class = MediaClass.VIDEO + can_play = True + thumb_mime_type = "image/jpeg" + + ret.append( + BrowseMediaSource( + domain=DOMAIN, + identifier=( + f"{identifier.unique_id}|albums|" + f"{identifier.collection_id}|" + f"{asset.asset_id}|" + f"{asset.original_file_name}|" + f"{mime_type}" + ), + media_class=media_class, + media_content_type=mime_type, + title=asset.original_file_name, + can_play=can_play, + can_expand=False, + thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/thumbnail/{thumb_mime_type}", + ) ) - for asset in album_info.assets - if (mime_type := asset.original_mime_type) - and mime_type.startswith("video/") - ) return ret From 1e304fad653529d018906d639718416929f3be41 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 2 Jun 2025 22:38:17 +0300 Subject: [PATCH 092/266] Fix Shelly BLU TRV calibrate button (#146066) --- homeassistant/components/shelly/button.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/shelly/button.py b/homeassistant/components/shelly/button.py index 44f81cc8b36..eab7514514d 100644 --- a/homeassistant/components/shelly/button.py +++ b/homeassistant/components/shelly/button.py @@ -7,7 +7,7 @@ from dataclasses import dataclass from functools import partial from typing import TYPE_CHECKING, Any, Final -from aioshelly.const import BLU_TRV_IDENTIFIER, MODEL_BLU_GATEWAY, RPC_GENERATIONS +from aioshelly.const import BLU_TRV_IDENTIFIER, MODEL_BLU_GATEWAY_G3, RPC_GENERATIONS from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCallError from homeassistant.components.button import ( @@ -62,7 +62,7 @@ BUTTONS: Final[list[ShellyButtonDescription[Any]]] = [ translation_key="self_test", entity_category=EntityCategory.DIAGNOSTIC, press_action="trigger_shelly_gas_self_test", - supported=lambda coordinator: coordinator.device.model in SHELLY_GAS_MODELS, + supported=lambda coordinator: coordinator.model in SHELLY_GAS_MODELS, ), ShellyButtonDescription[ShellyBlockCoordinator]( key="mute", @@ -70,7 +70,7 @@ BUTTONS: Final[list[ShellyButtonDescription[Any]]] = [ translation_key="mute", entity_category=EntityCategory.CONFIG, press_action="trigger_shelly_gas_mute", - supported=lambda coordinator: coordinator.device.model in SHELLY_GAS_MODELS, + supported=lambda coordinator: coordinator.model in SHELLY_GAS_MODELS, ), ShellyButtonDescription[ShellyBlockCoordinator]( key="unmute", @@ -78,7 +78,7 @@ BUTTONS: Final[list[ShellyButtonDescription[Any]]] = [ translation_key="unmute", entity_category=EntityCategory.CONFIG, press_action="trigger_shelly_gas_unmute", - supported=lambda coordinator: coordinator.device.model in SHELLY_GAS_MODELS, + supported=lambda coordinator: coordinator.model in SHELLY_GAS_MODELS, ), ] @@ -89,7 +89,7 @@ BLU_TRV_BUTTONS: Final[list[ShellyButtonDescription]] = [ translation_key="calibrate", entity_category=EntityCategory.CONFIG, press_action="trigger_blu_trv_calibration", - supported=lambda coordinator: coordinator.device.model == MODEL_BLU_GATEWAY, + supported=lambda coordinator: coordinator.model == MODEL_BLU_GATEWAY_G3, ), ] @@ -160,6 +160,7 @@ async def async_setup_entry( ShellyBluTrvButton(coordinator, button, id_) for id_ in blutrv_key_ids for button in BLU_TRV_BUTTONS + if button.supported(coordinator) ) async_add_entities(entities) From 1838a731d60b05e9f8c9b1f1db36dbb5ee71b297 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Tue, 3 Jun 2025 01:18:49 +0300 Subject: [PATCH 093/266] Bump aioamazondevices to 3.0.5 (#146073) --- homeassistant/components/amazon_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/amazon_devices/manifest.json b/homeassistant/components/amazon_devices/manifest.json index a24671298d9..bd9bc701d3e 100644 --- a/homeassistant/components/amazon_devices/manifest.json +++ b/homeassistant/components/amazon_devices/manifest.json @@ -118,5 +118,5 @@ "iot_class": "cloud_polling", "loggers": ["aioamazondevices"], "quality_scale": "bronze", - "requirements": ["aioamazondevices==3.0.4"] + "requirements": ["aioamazondevices==3.0.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4af2e16aa4a..890dc1cb1cb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -182,7 +182,7 @@ aioairzone-cloud==0.6.12 aioairzone==1.0.0 # homeassistant.components.amazon_devices -aioamazondevices==3.0.4 +aioamazondevices==3.0.5 # homeassistant.components.ambient_network # homeassistant.components.ambient_station diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2f51d93402b..5e472b51a61 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -170,7 +170,7 @@ aioairzone-cloud==0.6.12 aioairzone==1.0.0 # homeassistant.components.amazon_devices -aioamazondevices==3.0.4 +aioamazondevices==3.0.5 # homeassistant.components.ambient_network # homeassistant.components.ambient_station From 415858119a48dd3b282f7a650b0d726042816f5e Mon Sep 17 00:00:00 2001 From: SNoof85 Date: Tue, 3 Jun 2025 11:23:52 +0200 Subject: [PATCH 094/266] Add state class measurement to Freebox temperature sensors (#146074) --- homeassistant/components/freebox/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index 33af56a1f9e..45fe18db95a 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -84,6 +84,7 @@ async def async_setup_entry( name=f"Freebox {sensor_name}", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), ) for sensor_name in router.sensors_temperature From 010c5cab8783a8151d70722591737245cdf93f06 Mon Sep 17 00:00:00 2001 From: Retha Runolfsson <137745329+zerzhang@users.noreply.github.com> Date: Tue, 3 Jun 2025 18:11:02 +0800 Subject: [PATCH 095/266] Fix nightlatch option for all switchbot locks (#146090) --- homeassistant/components/switchbot/config_flow.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/switchbot/config_flow.py b/homeassistant/components/switchbot/config_flow.py index 04b4e20b7ce..82e6e43130b 100644 --- a/homeassistant/components/switchbot/config_flow.py +++ b/homeassistant/components/switchbot/config_flow.py @@ -367,7 +367,9 @@ class SwitchbotOptionsFlowHandler(OptionsFlow): ), ): int } - if self.config_entry.data.get(CONF_SENSOR_TYPE) == SupportedModels.LOCK_PRO: + if self.config_entry.data.get(CONF_SENSOR_TYPE, "").startswith( + SupportedModels.LOCK + ): options.update( { vol.Optional( From 81cbb6e5cfcea2dc170c691c67587228c7fb0a65 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Tue, 3 Jun 2025 18:40:20 +1000 Subject: [PATCH 096/266] Fix BMS and Charge states in Teslemetry (#146091) Fix BMS and Charge states --- homeassistant/components/teslemetry/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/teslemetry/sensor.py b/homeassistant/components/teslemetry/sensor.py index ab075d18132..8ddd7e186cb 100644 --- a/homeassistant/components/teslemetry/sensor.py +++ b/homeassistant/components/teslemetry/sensor.py @@ -205,7 +205,7 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryVehicleSensorEntityDescription, ...] = ( key="charge_state_charging_state", polling=True, streaming_listener=lambda vehicle, callback: vehicle.listen_DetailedChargeState( - lambda value: None if value is None else callback(value.lower()) + lambda value: callback(None if value is None else CHARGE_STATES.get(value)) ), polling_value_fn=lambda value: CHARGE_STATES.get(str(value)), options=list(CHARGE_STATES.values()), @@ -533,7 +533,7 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryVehicleSensorEntityDescription, ...] = ( TeslemetryVehicleSensorEntityDescription( key="bms_state", streaming_listener=lambda vehicle, callback: vehicle.listen_BMSState( - lambda value: None if value is None else callback(BMS_STATES.get(value)) + lambda value: callback(None if value is None else BMS_STATES.get(value)) ), device_class=SensorDeviceClass.ENUM, options=list(BMS_STATES.values()), From abfd443541a7c6f7a712e2b6b9ae5ce2cb907a7b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Jun 2025 11:57:58 +0100 Subject: [PATCH 097/266] Bump bleak-esphome to 2.16.0 (#146110) --- homeassistant/components/eq3btsmart/manifest.json | 2 +- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/eq3btsmart/manifest.json b/homeassistant/components/eq3btsmart/manifest.json index 1f619b2017c..889401ffc3e 100644 --- a/homeassistant/components/eq3btsmart/manifest.json +++ b/homeassistant/components/eq3btsmart/manifest.json @@ -22,5 +22,5 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["eq3btsmart"], - "requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.15.1"] + "requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.16.0"] } diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index d5faacfd1b0..d0bed1fdb4e 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -19,7 +19,7 @@ "requirements": [ "aioesphomeapi==31.1.0", "esphome-dashboard-api==1.3.0", - "bleak-esphome==2.15.1" + "bleak-esphome==2.16.0" ], "zeroconf": ["_esphomelib._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 890dc1cb1cb..4021c23edf4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -610,7 +610,7 @@ bizkaibus==0.1.1 # homeassistant.components.eq3btsmart # homeassistant.components.esphome -bleak-esphome==2.15.1 +bleak-esphome==2.16.0 # homeassistant.components.bluetooth bleak-retry-connector==3.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5e472b51a61..dca4fc91144 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -544,7 +544,7 @@ bimmer-connected[china]==0.17.2 # homeassistant.components.eq3btsmart # homeassistant.components.esphome -bleak-esphome==2.15.1 +bleak-esphome==2.16.0 # homeassistant.components.bluetooth bleak-retry-connector==3.9.0 From 1d578d856343739f67769ee63a02612c937c8b4d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Jun 2025 15:56:20 +0100 Subject: [PATCH 098/266] Bump habluetooth to 3.49.0 (#146111) * Bump habluetooth to 3.49.0 changelog: https://github.com/Bluetooth-Devices/habluetooth/compare/v3.48.2...v3.49.0 * update diag * diag --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/bluetooth/test_diagnostics.py | 1 + tests/components/esphome/test_diagnostics.py | 1 + tests/components/shelly/test_diagnostics.py | 7 ++++++- 7 files changed, 12 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 4fc835e4532..f212f4bdc17 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -21,6 +21,6 @@ "bluetooth-auto-recovery==1.5.2", "bluetooth-data-tools==1.28.1", "dbus-fast==2.43.0", - "habluetooth==3.48.2" + "habluetooth==3.49.0" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 598e2834e86..9cc65c62c7e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -34,7 +34,7 @@ dbus-fast==2.43.0 fnv-hash-fast==1.5.0 go2rtc-client==0.2.1 ha-ffmpeg==3.2.2 -habluetooth==3.48.2 +habluetooth==3.49.0 hass-nabucasa==0.101.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 diff --git a/requirements_all.txt b/requirements_all.txt index 4021c23edf4..5de5b884d5b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1121,7 +1121,7 @@ ha-silabs-firmware-client==0.2.0 habiticalib==0.4.0 # homeassistant.components.bluetooth -habluetooth==3.48.2 +habluetooth==3.49.0 # homeassistant.components.cloud hass-nabucasa==0.101.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dca4fc91144..de8f6f70f0c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -979,7 +979,7 @@ ha-silabs-firmware-client==0.2.0 habiticalib==0.4.0 # homeassistant.components.bluetooth -habluetooth==3.48.2 +habluetooth==3.49.0 # homeassistant.components.cloud hass-nabucasa==0.101.0 diff --git a/tests/components/bluetooth/test_diagnostics.py b/tests/components/bluetooth/test_diagnostics.py index 80fca88b2de..540bf1bfbd1 100644 --- a/tests/components/bluetooth/test_diagnostics.py +++ b/tests/components/bluetooth/test_diagnostics.py @@ -655,6 +655,7 @@ async def test_diagnostics_remote_adapter( "source": "esp32", "start_time": ANY, "time_since_last_device_detection": {"44:44:33:11:23:45": ANY}, + "raw_advertisement_data": {"44:44:33:11:23:45": None}, "type": "FakeScanner", }, ], diff --git a/tests/components/esphome/test_diagnostics.py b/tests/components/esphome/test_diagnostics.py index 84f2243a844..70acf327788 100644 --- a/tests/components/esphome/test_diagnostics.py +++ b/tests/components/esphome/test_diagnostics.py @@ -95,6 +95,7 @@ async def test_diagnostics_with_bluetooth( "scanning": True, "source": "AA:BB:CC:DD:EE:FC", "start_time": ANY, + "raw_advertisement_data": {}, "time_since_last_device_detection": {}, "type": "ESPHomeScanner", }, diff --git a/tests/components/shelly/test_diagnostics.py b/tests/components/shelly/test_diagnostics.py index 300b67abe75..6bd44fa036a 100644 --- a/tests/components/shelly/test_diagnostics.py +++ b/tests/components/shelly/test_diagnostics.py @@ -103,7 +103,6 @@ async def test_rpc_config_entry_diagnostics( ) result = await get_diagnostics_for_config_entry(hass, hass_client, entry) - assert result == { "entry": entry_dict | {"discovery_keys": {}}, "bluetooth": { @@ -152,6 +151,12 @@ async def test_rpc_config_entry_diagnostics( "start_time": ANY, "source": "12:34:56:78:9A:BE", "time_since_last_device_detection": {"AA:BB:CC:DD:EE:FF": ANY}, + "raw_advertisement_data": { + "AA:BB:CC:DD:EE:FF": { + "__type": "", + "repr": "b'\\x02\\x01\\x06\\t\\xffY\\x00\\xd1\\xfb;t\\xc8\\x90\\x11\\x07\\x1b\\xc5\\xd5\\xa5\\x02\\x00\\xb8\\x9f\\xe6\\x11M\"\\x00\\r\\xa2\\xcb\\x06\\x16\\x00\\rH\\x10a'", + } + }, "type": "ShellyBLEScanner", } }, From e8aab396204d4b446e62dee7ef24b77557082d23 Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Tue, 3 Jun 2025 21:54:44 +0200 Subject: [PATCH 099/266] SMA fix strings (#146112) * Fix * Feedback --- homeassistant/components/sma/strings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sma/strings.json b/homeassistant/components/sma/strings.json index e19acf20cf8..8253d94a749 100644 --- a/homeassistant/components/sma/strings.json +++ b/homeassistant/components/sma/strings.json @@ -34,10 +34,10 @@ "title": "Set up SMA Solar" }, "discovery_confirm": { - "title": "[%key:component::sma::config::step::user::title]", - "description": "Do you want to setup the discovered SMA ({host})?", + "title": "[%key:component::sma::config::step::user::title%]", + "description": "Do you want to set up the discovered SMA device ({host})?", "data": { - "group": "[%key:component::sma::config::step::user::data::group]", + "group": "[%key:component::sma::config::step::user::data::group%]", "password": "[%key:common::config_flow::data::password%]", "ssl": "[%key:common::config_flow::data::ssl%]", "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" From f71a1a7a899e48202974789ad35a6f551f800377 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 4 Jun 2025 07:40:10 +0100 Subject: [PATCH 100/266] Bump protobuf to 6.31.1 (#146128) changelog: https://github.com/protocolbuffers/protobuf/compare/v30.2...v31.1 --- homeassistant/package_constraints.txt | 2 +- script/gen_requirements_all.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 9cc65c62c7e..6df7d7d1802 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -145,7 +145,7 @@ iso4217!=1.10.20220401 # protobuf must be in package constraints for the wheel # builder to build binary wheels -protobuf==6.30.2 +protobuf==6.31.1 # faust-cchardet: Ensure we have a version we can build wheels # 2.1.18 is the first version that works with our wheel builder diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 606141e4c65..0ea69b365a2 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -172,7 +172,7 @@ iso4217!=1.10.20220401 # protobuf must be in package constraints for the wheel # builder to build binary wheels -protobuf==6.30.2 +protobuf==6.31.1 # faust-cchardet: Ensure we have a version we can build wheels # 2.1.18 is the first version that works with our wheel builder From bfb140d2e94e25be4662d22ced33a0a3d5c0b635 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 4 Jun 2025 09:34:04 +0100 Subject: [PATCH 101/266] Bump aioesphomeapi to 32.0.0 (#146135) --- 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 d0bed1fdb4e..eea0ed060f9 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==31.1.0", + "aioesphomeapi==32.0.0", "esphome-dashboard-api==1.3.0", "bleak-esphome==2.16.0" ], diff --git a/requirements_all.txt b/requirements_all.txt index 5de5b884d5b..ee5f2f3d8d3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -244,7 +244,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==31.1.0 +aioesphomeapi==32.0.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index de8f6f70f0c..f544b68f573 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -232,7 +232,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==31.1.0 +aioesphomeapi==32.0.0 # homeassistant.components.flo aioflo==2021.11.0 From 6c098c3e0a7b6ca2017fe9fb63fbac8d8255f5a5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 4 Jun 2025 09:02:53 +0000 Subject: [PATCH 102/266] Bump version to 2025.6.0b5 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index edbdba419f3..25d722ea685 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 = 6 -PATCH_VERSION: Final = "0b4" +PATCH_VERSION: Final = "0b5" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index aa9de97d73b..7abfb13f248 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.6.0b4" +version = "2025.6.0b5" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From 78d2bf736c07c37759b5d8583a0e202e412f3ec2 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 10 Jun 2025 18:05:55 +0200 Subject: [PATCH 103/266] Reolink conserve battery (#145452) --- homeassistant/components/reolink/__init__.py | 59 ++++++++++++--- homeassistant/components/reolink/const.py | 6 ++ homeassistant/components/reolink/entity.py | 4 +- homeassistant/components/reolink/host.py | 46 +++++++++--- tests/components/reolink/test_init.py | 77 +++++++++++++++++++- 5 files changed, 170 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index 38f8e709b5c..aa4ee36fc67 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -3,8 +3,10 @@ from __future__ import annotations import asyncio +from collections.abc import Callable from datetime import timedelta import logging +from time import time from typing import Any from reolink_aio.api import RETRY_ATTEMPTS @@ -28,7 +30,13 @@ from homeassistant.helpers.event import async_call_later from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import CONF_BC_PORT, CONF_SUPPORTS_PRIVACY_MODE, CONF_USE_HTTPS, DOMAIN +from .const import ( + BATTERY_PASSIVE_WAKE_UPDATE_INTERVAL, + CONF_BC_PORT, + CONF_SUPPORTS_PRIVACY_MODE, + CONF_USE_HTTPS, + DOMAIN, +) from .exceptions import PasswordIncompatible, ReolinkException, UserNotAdmin from .host import ReolinkHost from .services import async_setup_services @@ -220,6 +228,24 @@ async def async_setup_entry( hass.http.register_view(PlaybackProxyView(hass)) + await register_callbacks(host, device_coordinator, hass) + + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) + + config_entry.async_on_unload( + config_entry.add_update_listener(entry_update_listener) + ) + + return True + + +async def register_callbacks( + host: ReolinkHost, + device_coordinator: DataUpdateCoordinator[None], + hass: HomeAssistant, +) -> None: + """Register update callbacks.""" + async def refresh(*args: Any) -> None: """Request refresh of coordinator.""" await device_coordinator.async_request_refresh() @@ -233,17 +259,29 @@ async def async_setup_entry( host.cancel_refresh_privacy_mode = async_call_later(hass, 2, refresh) host.privacy_mode = host.api.baichuan.privacy_mode() + def generate_async_camera_wake(channel: int) -> Callable[[], None]: + def async_camera_wake() -> None: + """Request update when a battery camera wakes up.""" + if ( + not host.api.sleeping(channel) + and time() - host.last_wake[channel] + > BATTERY_PASSIVE_WAKE_UPDATE_INTERVAL + ): + hass.loop.create_task(device_coordinator.async_request_refresh()) + + return async_camera_wake + host.api.baichuan.register_callback( "privacy_mode_change", async_privacy_mode_change, 623 ) - - await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) - - config_entry.async_on_unload( - config_entry.add_update_listener(entry_update_listener) - ) - - return True + for channel in host.api.channels: + if host.api.supported(channel, "battery"): + host.api.baichuan.register_callback( + f"camera_{channel}_wake", + generate_async_camera_wake(channel), + 145, + channel, + ) async def entry_update_listener( @@ -262,6 +300,9 @@ async def async_unload_entry( await host.stop() host.api.baichuan.unregister_callback("privacy_mode_change") + for channel in host.api.channels: + if host.api.supported(channel, "battery"): + host.api.baichuan.unregister_callback(f"camera_{channel}_wake") if host.cancel_refresh_privacy_mode is not None: host.cancel_refresh_privacy_mode() diff --git a/homeassistant/components/reolink/const.py b/homeassistant/components/reolink/const.py index 026d1219881..bd9c4bb84a2 100644 --- a/homeassistant/components/reolink/const.py +++ b/homeassistant/components/reolink/const.py @@ -5,3 +5,9 @@ DOMAIN = "reolink" CONF_USE_HTTPS = "use_https" CONF_BC_PORT = "baichuan_port" CONF_SUPPORTS_PRIVACY_MODE = "privacy_mode_supported" + +# Conserve battery by not waking the battery cameras each minute during normal update +# Most props are cached in the Home Hub and updated, but some are skipped +BATTERY_PASSIVE_WAKE_UPDATE_INTERVAL = 3600 # seconds +BATTERY_WAKE_UPDATE_INTERVAL = 6 * BATTERY_PASSIVE_WAKE_UPDATE_INTERVAL +BATTERY_ALL_WAKE_UPDATE_INTERVAL = 2 * BATTERY_WAKE_UPDATE_INTERVAL diff --git a/homeassistant/components/reolink/entity.py b/homeassistant/components/reolink/entity.py index f2a0b20994a..d7e8817b1b7 100644 --- a/homeassistant/components/reolink/entity.py +++ b/homeassistant/components/reolink/entity.py @@ -142,7 +142,9 @@ class ReolinkHostCoordinatorEntity(CoordinatorEntity[DataUpdateCoordinator[None] async def async_update(self) -> None: """Force full update from the generic entity update service.""" - self._host.last_wake = 0 + for channel in self._host.api.channels: + if self._host.api.supported(channel, "battery"): + self._host.last_wake[channel] = 0 await super().async_update() diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index c3a8d340501..39b58c92ac3 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -34,7 +34,15 @@ from homeassistant.helpers.network import NoURLAvailableError, get_url from homeassistant.helpers.storage import Store from homeassistant.util.ssl import SSLCipherList -from .const import CONF_BC_PORT, CONF_SUPPORTS_PRIVACY_MODE, CONF_USE_HTTPS, DOMAIN +from .const import ( + BATTERY_ALL_WAKE_UPDATE_INTERVAL, + BATTERY_PASSIVE_WAKE_UPDATE_INTERVAL, + BATTERY_WAKE_UPDATE_INTERVAL, + CONF_BC_PORT, + CONF_SUPPORTS_PRIVACY_MODE, + CONF_USE_HTTPS, + DOMAIN, +) from .exceptions import ( PasswordIncompatible, ReolinkSetupException, @@ -52,10 +60,6 @@ POLL_INTERVAL_NO_PUSH = 5 LONG_POLL_COOLDOWN = 0.75 LONG_POLL_ERROR_COOLDOWN = 30 -# Conserve battery by not waking the battery cameras each minute during normal update -# Most props are cached in the Home Hub and updated, but some are skipped -BATTERY_WAKE_UPDATE_INTERVAL = 3600 # seconds - _LOGGER = logging.getLogger(__name__) @@ -95,7 +99,8 @@ class ReolinkHost: bc_port=config.get(CONF_BC_PORT, DEFAULT_BC_PORT), ) - self.last_wake: float = 0 + self.last_wake: defaultdict[int, float] = defaultdict(float) + self.last_all_wake: float = 0 self.update_cmd: defaultdict[str, defaultdict[int | None, int]] = defaultdict( lambda: defaultdict(int) ) @@ -459,15 +464,34 @@ class ReolinkHost: async def update_states(self) -> None: """Call the API of the camera device to update the internal states.""" - wake = False - if time() - self.last_wake > BATTERY_WAKE_UPDATE_INTERVAL: + wake: dict[int, bool] = {} + now = time() + for channel in self._api.stream_channels: # wake the battery cameras for a complete update - wake = True - self.last_wake = time() + if not self._api.supported(channel, "battery"): + wake[channel] = True + elif ( + ( + not self._api.sleeping(channel) + and now - self.last_wake[channel] + > BATTERY_PASSIVE_WAKE_UPDATE_INTERVAL + ) + or (now - self.last_wake[channel] > BATTERY_WAKE_UPDATE_INTERVAL) + or (now - self.last_all_wake > BATTERY_ALL_WAKE_UPDATE_INTERVAL) + ): + # let a waking update coincide with the camera waking up by itself unless it did not wake for BATTERY_WAKE_UPDATE_INTERVAL + wake[channel] = True + self.last_wake[channel] = now + else: + wake[channel] = False - for channel in self._api.channels: + # check privacy mode if enabled if self._api.baichuan.privacy_mode(channel): await self._api.baichuan.get_privacy_mode(channel) + + if all(wake.values()): + self.last_all_wake = now + if self._api.baichuan.privacy_mode(): return # API is shutdown, no need to check states diff --git a/tests/components/reolink/test_init.py b/tests/components/reolink/test_init.py index 3551632903f..86c4ed861a1 100644 --- a/tests/components/reolink/test_init.py +++ b/tests/components/reolink/test_init.py @@ -19,7 +19,12 @@ from homeassistant.components.reolink import ( FIRMWARE_UPDATE_INTERVAL, NUM_CRED_ERRORS, ) -from homeassistant.components.reolink.const import CONF_BC_PORT, DOMAIN +from homeassistant.components.reolink.const import ( + BATTERY_ALL_WAKE_UPDATE_INTERVAL, + BATTERY_PASSIVE_WAKE_UPDATE_INTERVAL, + CONF_BC_PORT, + DOMAIN, +) from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( CONF_HOST, @@ -1111,6 +1116,76 @@ async def test_privacy_mode_change_callback( assert config_entry.state is ConfigEntryState.NOT_LOADED +async def test_camera_wake_callback( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + config_entry: MockConfigEntry, + reolink_connect: MagicMock, +) -> None: + """Test camera wake callback.""" + + class callback_mock_class: + callback_func = None + + def register_callback( + self, callback_id: str, callback: Callable[[], None], *args, **key_args + ) -> None: + if callback_id == "camera_0_wake": + self.callback_func = callback + + callback_mock = callback_mock_class() + + reolink_connect.model = TEST_HOST_MODEL + reolink_connect.baichuan.events_active = True + reolink_connect.baichuan.subscribe_events.reset_mock(side_effect=True) + reolink_connect.baichuan.register_callback = callback_mock.register_callback + reolink_connect.sleeping.return_value = True + reolink_connect.audio_record.return_value = True + reolink_connect.get_states = AsyncMock() + + with ( + patch("homeassistant.components.reolink.PLATFORMS", [Platform.SWITCH]), + patch( + "homeassistant.components.reolink.host.time", + return_value=BATTERY_ALL_WAKE_UPDATE_INTERVAL, + ), + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.LOADED + + entity_id = f"{Platform.SWITCH}.{TEST_NVR_NAME}_record_audio" + assert hass.states.get(entity_id).state == STATE_ON + + reolink_connect.sleeping.return_value = False + reolink_connect.get_states.reset_mock() + assert reolink_connect.get_states.call_count == 0 + + # simulate a TCP push callback signaling the battery camera woke up + reolink_connect.audio_record.return_value = False + assert callback_mock.callback_func is not None + with ( + patch( + "homeassistant.components.reolink.host.time", + return_value=BATTERY_ALL_WAKE_UPDATE_INTERVAL + + BATTERY_PASSIVE_WAKE_UPDATE_INTERVAL + + 5, + ), + patch( + "homeassistant.components.reolink.time", + return_value=BATTERY_ALL_WAKE_UPDATE_INTERVAL + + BATTERY_PASSIVE_WAKE_UPDATE_INTERVAL + + 5, + ), + ): + callback_mock.callback_func() + await hass.async_block_till_done() + + # check that a coordinator update was scheduled. + assert reolink_connect.get_states.call_count >= 1 + assert hass.states.get(entity_id).state == STATE_OFF + + async def test_remove( hass: HomeAssistant, reolink_connect: MagicMock, From 5821b2f03ccd3519b137a77c677592dca26d6f81 Mon Sep 17 00:00:00 2001 From: Arie Catsman <120491684+catsmanac@users.noreply.github.com> Date: Tue, 10 Jun 2025 16:25:26 +0200 Subject: [PATCH 104/266] fix possible mac collision in enphase_envoy (#145549) * fix possible mac collision in enphase_envoy * remove redundant device registry async_get --- .../components/enphase_envoy/coordinator.py | 12 +++- tests/components/enphase_envoy/test_init.py | 58 ++++++++++++++++++- 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/enphase_envoy/coordinator.py b/homeassistant/components/enphase_envoy/coordinator.py index 40c690b29ec..cfff0777af5 100644 --- a/homeassistant/components/enphase_envoy/coordinator.py +++ b/homeassistant/components/enphase_envoy/coordinator.py @@ -180,9 +180,15 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): ) return - device_registry.async_update_device( - device_id=envoy_device.id, - new_connections={connection}, + device_registry.async_get_or_create( + config_entry_id=self.config_entry.entry_id, + identifiers={ + ( + DOMAIN, + self.envoy_serial_number, + ) + }, + connections={connection}, ) _LOGGER.debug("added connection: %s to %s", connection, self.name) diff --git a/tests/components/enphase_envoy/test_init.py b/tests/components/enphase_envoy/test_init.py index ef071b421fe..560d0719424 100644 --- a/tests/components/enphase_envoy/test_init.py +++ b/tests/components/enphase_envoy/test_init.py @@ -510,7 +510,6 @@ async def test_coordinator_interface_information_no_device( ) # update device to force no device found in mac verification - device_registry = dr.async_get(hass) envoy_device = device_registry.async_get_device( identifiers={ ( @@ -531,3 +530,60 @@ async def test_coordinator_interface_information_no_device( # verify no device found message in log assert "No envoy device found in device registry" in caplog.text + + +@respx.mock +async def test_coordinator_interface_information_mac_also_in_other_device( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_envoy: AsyncMock, + freezer: FrozenDateTimeFactory, + caplog: pytest.LogCaptureFixture, + device_registry: dr.DeviceRegistry, +) -> None: + """Test coordinator interface mac verification with MAC also in other existing device.""" + await setup_integration(hass, config_entry) + + caplog.set_level(logging.DEBUG) + logging.getLogger("homeassistant.components.enphase_envoy.coordinator").setLevel( + logging.DEBUG + ) + + # add existing device with MAC and sparsely populated i.e. unifi that found envoy + other_config_entry = MockConfigEntry(domain="test", data={}) + other_config_entry.add_to_hass(hass) + device_registry.async_get_or_create( + config_entry_id=other_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "00:11:22:33:44:55")}, + manufacturer="Enphase Energy", + ) + + envoy_device = device_registry.async_get_device( + identifiers={ + ( + DOMAIN, + mock_envoy.serial_number, + ) + } + ) + assert envoy_device + + # move time forward so interface information is fetched + freezer.tick(MAC_VERIFICATION_DELAY) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + # verify mac was added + assert "added connection: ('mac', '00:11:22:33:44:55') to Envoy 1234" in caplog.text + + # verify connection is now in envoy device + envoy_device_refetched = device_registry.async_get(envoy_device.id) + assert envoy_device_refetched + assert envoy_device_refetched.name == "Envoy 1234" + assert envoy_device_refetched.serial_number == "1234" + assert envoy_device_refetched.connections == { + ( + dr.CONNECTION_NETWORK_MAC, + "00:11:22:33:44:55", + ) + } From 41431282ee938d2dcc410dd744625801b7b88699 Mon Sep 17 00:00:00 2001 From: Andrea Turri Date: Tue, 10 Jun 2025 16:23:55 +0200 Subject: [PATCH 105/266] Add evaporate water program id for Miele oven (#145996) --- homeassistant/components/miele/const.py | 1 + homeassistant/components/miele/strings.json | 1 + 2 files changed, 2 insertions(+) diff --git a/homeassistant/components/miele/const.py b/homeassistant/components/miele/const.py index 0d11cbdd0a5..bda276c6d8a 100644 --- a/homeassistant/components/miele/const.py +++ b/homeassistant/components/miele/const.py @@ -527,6 +527,7 @@ OVEN_PROGRAM_ID: dict[int, str] = { 116: "custom_program_20", 323: "pyrolytic", 326: "descale", + 327: "evaporate_water", 335: "shabbat_program", 336: "yom_tov", 356: "defrost", diff --git a/homeassistant/components/miele/strings.json b/homeassistant/components/miele/strings.json index 6774d813e44..cf01d01e476 100644 --- a/homeassistant/components/miele/strings.json +++ b/homeassistant/components/miele/strings.json @@ -542,6 +542,7 @@ "endive_strips": "Endive (strips)", "espresso": "Espresso", "espresso_macchiato": "Espresso macchiato", + "evaporate_water": "Evaporate water", "express": "Express", "express_20": "Express 20'", "extra_quiet": "Extra quiet", From dfc4889d456409becb7d9015f8554f052de6c319 Mon Sep 17 00:00:00 2001 From: Ian Date: Tue, 10 Jun 2025 06:03:20 -0700 Subject: [PATCH 106/266] Throttle Nextbus if we are reaching the rate limit (#146064) Co-authored-by: Josef Zweck Co-authored-by: Robert Resch --- .../components/nextbus/coordinator.py | 35 ++++++++++--- tests/components/nextbus/conftest.py | 7 +++ tests/components/nextbus/test_sensor.py | 52 +++++++++++++++++++ 3 files changed, 88 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/nextbus/coordinator.py b/homeassistant/components/nextbus/coordinator.py index 617669adf2f..e8d7ab06915 100644 --- a/homeassistant/components/nextbus/coordinator.py +++ b/homeassistant/components/nextbus/coordinator.py @@ -1,8 +1,8 @@ """NextBus data update coordinator.""" -from datetime import timedelta +from datetime import datetime, timedelta import logging -from typing import Any +from typing import Any, override from py_nextbus import NextBusClient from py_nextbus.client import NextBusFormatError, NextBusHTTPError @@ -15,8 +15,14 @@ from .util import RouteStop _LOGGER = logging.getLogger(__name__) +# At what percentage of the request limit should the coordinator pause making requests +UPDATE_INTERVAL_SECONDS = 30 +THROTTLE_PRECENTAGE = 80 -class NextBusDataUpdateCoordinator(DataUpdateCoordinator): + +class NextBusDataUpdateCoordinator( + DataUpdateCoordinator[dict[RouteStop, dict[str, Any]]] +): """Class to manage fetching NextBus data.""" def __init__(self, hass: HomeAssistant, agency: str) -> None: @@ -26,7 +32,7 @@ class NextBusDataUpdateCoordinator(DataUpdateCoordinator): _LOGGER, config_entry=None, # It is shared between multiple entries name=DOMAIN, - update_interval=timedelta(seconds=30), + update_interval=timedelta(seconds=UPDATE_INTERVAL_SECONDS), ) self.client = NextBusClient(agency_id=agency) self._agency = agency @@ -49,9 +55,26 @@ class NextBusDataUpdateCoordinator(DataUpdateCoordinator): """Check if this coordinator is tracking any routes.""" return len(self._route_stops) > 0 - async def _async_update_data(self) -> dict[str, Any]: + @override + async def _async_update_data(self) -> dict[RouteStop, dict[str, Any]]: """Fetch data from NextBus.""" + if ( + # If we have predictions, check the rate limit + self._predictions + # If are over our rate limit percentage, we should throttle + and self.client.rate_limit_percent >= THROTTLE_PRECENTAGE + # But only if we have a reset time to unthrottle + and self.client.rate_limit_reset is not None + # Unless we are after the reset time + and datetime.now() < self.client.rate_limit_reset + ): + self.logger.debug( + "Rate limit threshold reached. Skipping updates for. Routes: %s", + str(self._route_stops), + ) + return self._predictions + _stops_to_route_stops: dict[str, set[RouteStop]] = {} for route_stop in self._route_stops: _stops_to_route_stops.setdefault(route_stop.stop_id, set()).add(route_stop) @@ -60,7 +83,7 @@ class NextBusDataUpdateCoordinator(DataUpdateCoordinator): "Updating data from API. Routes: %s", str(_stops_to_route_stops) ) - def _update_data() -> dict: + def _update_data() -> dict[RouteStop, dict[str, Any]]: """Fetch data from NextBus.""" self.logger.debug("Updating data from API (executor)") predictions: dict[RouteStop, dict[str, Any]] = {} diff --git a/tests/components/nextbus/conftest.py b/tests/components/nextbus/conftest.py index 3f687989313..9891f6ffa49 100644 --- a/tests/components/nextbus/conftest.py +++ b/tests/components/nextbus/conftest.py @@ -137,6 +137,13 @@ def mock_nextbus_lists( def mock_nextbus() -> Generator[MagicMock]: """Create a mock py_nextbus module.""" with patch("homeassistant.components.nextbus.coordinator.NextBusClient") as client: + instance = client.return_value + + # Set some mocked rate limit values + instance.rate_limit = 450 + instance.rate_limit_remaining = 225 + instance.rate_limit_percent = 50.0 + yield client diff --git a/tests/components/nextbus/test_sensor.py b/tests/components/nextbus/test_sensor.py index 04140a17c4f..eacab5cd5c4 100644 --- a/tests/components/nextbus/test_sensor.py +++ b/tests/components/nextbus/test_sensor.py @@ -1,6 +1,7 @@ """The tests for the nexbus sensor component.""" from copy import deepcopy +from datetime import datetime, timedelta from unittest.mock import MagicMock from urllib.error import HTTPError @@ -122,6 +123,57 @@ async def test_verify_no_upcoming( assert state.state == "unknown" +async def test_verify_throttle( + hass: HomeAssistant, + mock_nextbus: MagicMock, + mock_nextbus_lists: MagicMock, + mock_nextbus_predictions: MagicMock, + freezer: FrozenDateTimeFactory, +) -> None: + """Verify that the sensor coordinator is throttled correctly.""" + + # Set rate limit past threshold, should be ignored for first request + mock_client = mock_nextbus.return_value + mock_client.rate_limit_percent = 99.0 + mock_client.rate_limit_reset = datetime.now() + timedelta(seconds=30) + + # Do a request with the initial config and get predictions + await assert_setup_sensor(hass, CONFIG_BASIC) + + # Validate the predictions are present + state = hass.states.get(SENSOR_ID) + assert state is not None + assert state.state == "2019-03-28T21:09:31+00:00" + assert state.attributes["agency"] == VALID_AGENCY + assert state.attributes["route"] == VALID_ROUTE_TITLE + assert state.attributes["stop"] == VALID_STOP_TITLE + assert state.attributes["upcoming"] == "1, 2, 3, 10" + + # Update the predictions mock to return a different result + mock_nextbus_predictions.return_value = NO_UPCOMING + + # Move time forward and bump the rate limit reset time + mock_client.rate_limit_reset = freezer.tick(31) + timedelta(seconds=30) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + # Verify that the sensor state is unchanged + state = hass.states.get(SENSOR_ID) + assert state is not None + assert state.state == "2019-03-28T21:09:31+00:00" + + # Move time forward past the rate limit reset time + freezer.tick(31) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + # Verify that the sensor state is updated with the new predictions + state = hass.states.get(SENSOR_ID) + assert state is not None + assert state.attributes["upcoming"] == "No upcoming predictions" + assert state.state == "unknown" + + async def test_unload_entry( hass: HomeAssistant, mock_nextbus: MagicMock, From ce76b5db1657bcb2677a90e5311ea1cf93a5237c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 4 Jun 2025 12:57:40 +0100 Subject: [PATCH 107/266] Bump aiohttp to 3.12.8 (#146153) --- 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 6df7d7d1802..3cfcc2e0a9e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -6,7 +6,7 @@ aiodns==3.4.0 aiohasupervisor==0.3.1 aiohttp-asyncmdnsresolver==0.1.1 aiohttp-fast-zlib==0.2.3 -aiohttp==3.12.6 +aiohttp==3.12.8 aiohttp_cors==0.7.0 aiousbwatcher==1.1.1 aiozoneinfo==0.2.3 diff --git a/pyproject.toml b/pyproject.toml index 7abfb13f248..1921b1550b6 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.6", + "aiohttp==3.12.8", "aiohttp_cors==0.7.0", "aiohttp-fast-zlib==0.2.3", "aiohttp-asyncmdnsresolver==0.1.1", diff --git a/requirements.txt b/requirements.txt index a9a3e105f33..17b01971087 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ # Home Assistant Core aiodns==3.4.0 aiohasupervisor==0.3.1 -aiohttp==3.12.6 +aiohttp==3.12.8 aiohttp_cors==0.7.0 aiohttp-fast-zlib==0.2.3 aiohttp-asyncmdnsresolver==0.1.1 From 38c92a2338f2b2f354459e1e19a6504fd32aa06f Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 4 Jun 2025 13:35:16 +0200 Subject: [PATCH 108/266] Bump aioimmich to 0.9.0 (#146154) bump aioimmich to 0.9.0 --- homeassistant/components/immich/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/immich/manifest.json b/homeassistant/components/immich/manifest.json index b7c8176356f..1a4ccc8580c 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.8.0"] + "requirements": ["aioimmich==0.9.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index ee5f2f3d8d3..173f7f94c2a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -280,7 +280,7 @@ aiohue==4.7.4 aioimaplib==2.0.1 # homeassistant.components.immich -aioimmich==0.8.0 +aioimmich==0.9.0 # homeassistant.components.apache_kafka aiokafka==0.10.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f544b68f573..fcca7761e2e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -265,7 +265,7 @@ aiohue==4.7.4 aioimaplib==2.0.1 # homeassistant.components.immich -aioimmich==0.8.0 +aioimmich==0.9.0 # homeassistant.components.apache_kafka aiokafka==0.10.0 From d8759898662e47c4b51b50f0be934e07204f98f7 Mon Sep 17 00:00:00 2001 From: Iskra kranj <162285659+iskrakranj@users.noreply.github.com> Date: Wed, 4 Jun 2025 14:51:40 +0200 Subject: [PATCH 109/266] Bump pyiskra to 0.1.21 (#146156) --- homeassistant/components/iskra/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/iskra/manifest.json b/homeassistant/components/iskra/manifest.json index 3f7c805a917..da983db9969 100644 --- a/homeassistant/components/iskra/manifest.json +++ b/homeassistant/components/iskra/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["pyiskra"], - "requirements": ["pyiskra==0.1.19"] + "requirements": ["pyiskra==0.1.21"] } diff --git a/requirements_all.txt b/requirements_all.txt index 173f7f94c2a..217d94bf1a6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2051,7 +2051,7 @@ pyiqvia==2022.04.0 pyirishrail==0.0.2 # homeassistant.components.iskra -pyiskra==0.1.19 +pyiskra==0.1.21 # homeassistant.components.iss pyiss==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fcca7761e2e..e6b68640306 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1702,7 +1702,7 @@ pyipp==0.17.0 pyiqvia==2022.04.0 # homeassistant.components.iskra -pyiskra==0.1.19 +pyiskra==0.1.21 # homeassistant.components.iss pyiss==1.0.1 From 5accc3dec2ae521bfe21451e4261849097287adf Mon Sep 17 00:00:00 2001 From: Raphael Hehl <7577984+RaHehl@users.noreply.github.com> Date: Wed, 4 Jun 2025 22:32:44 +0200 Subject: [PATCH 110/266] Bump uiprotect to 7.11.0 (#146171) Bump uiprotect to version 7.11.0 --- 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 1cf2e4391e2..64bb278a8e2 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.10.1", "unifi-discovery==1.2.0"], + "requirements": ["uiprotect==7.11.0", "unifi-discovery==1.2.0"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 217d94bf1a6..af40384991e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2987,7 +2987,7 @@ typedmonarchmoney==0.4.4 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==7.10.1 +uiprotect==7.11.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e6b68640306..b5c4c9e7760 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2458,7 +2458,7 @@ typedmonarchmoney==0.4.4 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==7.10.1 +uiprotect==7.11.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From 8312780c477edd2508fbc2468d808d49a1643bd7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 4 Jun 2025 19:12:19 +0100 Subject: [PATCH 111/266] Bump aiohttp to 3.12.9 (#146178) --- 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 3cfcc2e0a9e..9b5a389e05b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -6,7 +6,7 @@ aiodns==3.4.0 aiohasupervisor==0.3.1 aiohttp-asyncmdnsresolver==0.1.1 aiohttp-fast-zlib==0.2.3 -aiohttp==3.12.8 +aiohttp==3.12.9 aiohttp_cors==0.7.0 aiousbwatcher==1.1.1 aiozoneinfo==0.2.3 diff --git a/pyproject.toml b/pyproject.toml index 1921b1550b6..2e0d06707dc 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.8", + "aiohttp==3.12.9", "aiohttp_cors==0.7.0", "aiohttp-fast-zlib==0.2.3", "aiohttp-asyncmdnsresolver==0.1.1", diff --git a/requirements.txt b/requirements.txt index 17b01971087..8c65e9a0b5a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ # Home Assistant Core aiodns==3.4.0 aiohasupervisor==0.3.1 -aiohttp==3.12.8 +aiohttp==3.12.9 aiohttp_cors==0.7.0 aiohttp-fast-zlib==0.2.3 aiohttp-asyncmdnsresolver==0.1.1 From e4140d71abe646e7b58275aaec4a373a2607c1bf Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Tue, 10 Jun 2025 23:00:02 +1000 Subject: [PATCH 112/266] Prevent energy history returning zero in Teslemetry (#146202) --- homeassistant/components/teslemetry/coordinator.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/teslemetry/coordinator.py b/homeassistant/components/teslemetry/coordinator.py index 406b9cb2d84..c31bdc2a34e 100644 --- a/homeassistant/components/teslemetry/coordinator.py +++ b/homeassistant/components/teslemetry/coordinator.py @@ -195,9 +195,13 @@ class TeslemetryEnergyHistoryCoordinator(DataUpdateCoordinator[dict[str, Any]]): raise UpdateFailed(e.message) from e # Add all time periods together - output = dict.fromkeys(ENERGY_HISTORY_FIELDS, 0) + output = dict.fromkeys(ENERGY_HISTORY_FIELDS, None) for period in data.get("time_series", []): for key in ENERGY_HISTORY_FIELDS: - output[key] += period.get(key, 0) + if key in period: + if output[key] is None: + output[key] = period[key] + else: + output[key] += period[key] return output From e5dd15da82234c346c709e0983e6e126070233a9 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Fri, 6 Jun 2025 00:39:55 +1000 Subject: [PATCH 113/266] Fix Export Rule Select Entity in Tessie (#146203) Fix TessieExportRuleSelectEntity --- homeassistant/components/tessie/select.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tessie/select.py b/homeassistant/components/tessie/select.py index 471372a68bd..ce907deb9c8 100644 --- a/homeassistant/components/tessie/select.py +++ b/homeassistant/components/tessie/select.py @@ -168,6 +168,8 @@ class TessieExportRuleSelectEntity(TessieEnergyEntity, SelectEntity): async def async_select_option(self, option: str) -> None: """Change the selected option.""" - await handle_command(self.api.grid_import_export(option)) + await handle_command( + self.api.grid_import_export(customer_preferred_export_rule=option) + ) self._attr_current_option = option self.async_write_ha_state() From fc8b5129314ac4c7fda5970439c6c3bfbb9d1983 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 5 Jun 2025 18:02:11 +0200 Subject: [PATCH 114/266] Remove zeroconf discovery from Spotify (#146213) --- .../components/spotify/manifest.json | 3 +- homeassistant/components/spotify/strings.json | 3 -- homeassistant/generated/zeroconf.py | 5 --- tests/components/spotify/test_config_flow.py | 41 +------------------ 4 files changed, 2 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/spotify/manifest.json b/homeassistant/components/spotify/manifest.json index 27b8da7cecf..80fcc777e73 100644 --- a/homeassistant/components/spotify/manifest.json +++ b/homeassistant/components/spotify/manifest.json @@ -8,6 +8,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["spotifyaio"], - "requirements": ["spotifyaio==0.8.11"], - "zeroconf": ["_spotify-connect._tcp.local."] + "requirements": ["spotifyaio==0.8.11"] } diff --git a/homeassistant/components/spotify/strings.json b/homeassistant/components/spotify/strings.json index 303942803be..66d837c503f 100644 --- a/homeassistant/components/spotify/strings.json +++ b/homeassistant/components/spotify/strings.json @@ -7,9 +7,6 @@ "reauth_confirm": { "title": "[%key:common::config_flow::title::reauth%]", "description": "The Spotify integration needs to re-authenticate with Spotify for account: {account}" - }, - "oauth_discovery": { - "description": "Home Assistant has found Spotify on your network. Press **Submit** to continue setting up Spotify." } }, "abort": { diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index ed5ac37c0cd..e675a0bb237 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -865,11 +865,6 @@ ZEROCONF = { "domain": "soundtouch", }, ], - "_spotify-connect._tcp.local.": [ - { - "domain": "spotify", - }, - ], "_ssh._tcp.local.": [ { "domain": "smappee", diff --git a/tests/components/spotify/test_config_flow.py b/tests/components/spotify/test_config_flow.py index 0f48002e5db..31842253c0c 100644 --- a/tests/components/spotify/test_config_flow.py +++ b/tests/components/spotify/test_config_flow.py @@ -1,33 +1,21 @@ """Tests for the Spotify config flow.""" from http import HTTPStatus -from ipaddress import ip_address from unittest.mock import MagicMock, patch import pytest from spotifyaio import SpotifyConnectionError from homeassistant.components.spotify.const import DOMAIN -from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF +from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow -from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker from tests.typing import ClientSessionGenerator -BLANK_ZEROCONF_INFO = ZeroconfServiceInfo( - ip_address=ip_address("1.2.3.4"), - ip_addresses=[ip_address("1.2.3.4")], - hostname="mock_hostname", - name="mock_name", - port=None, - properties={}, - type="mock_type", -) - async def test_abort_if_no_configuration(hass: HomeAssistant) -> None: """Check flow aborts when no configuration is present.""" @@ -39,18 +27,6 @@ async def test_abort_if_no_configuration(hass: HomeAssistant) -> None: assert result["reason"] == "missing_credentials" -async def test_zeroconf_abort_if_existing_entry(hass: HomeAssistant) -> None: - """Check zeroconf flow aborts when an entry already exist.""" - MockConfigEntry(domain=DOMAIN).add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_ZEROCONF}, data=BLANK_ZEROCONF_INFO - ) - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "already_configured" - - @pytest.mark.usefixtures("current_request_with_host") @pytest.mark.usefixtures("setup_credentials") async def test_full_flow( @@ -258,18 +234,3 @@ async def test_reauth_account_mismatch( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_account_mismatch" - - -async def test_zeroconf(hass: HomeAssistant) -> None: - """Check zeroconf flow aborts when an entry already exist.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_ZEROCONF}, data=BLANK_ZEROCONF_INFO - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "oauth_discovery" - - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "missing_credentials" From f6a4486c6567fece2091016aaade569e58f47142 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 10 Jun 2025 14:10:35 +0200 Subject: [PATCH 115/266] Explain Withings setup (#146216) --- .../components/withings/application_credentials.py | 8 ++++++++ homeassistant/components/withings/strings.json | 5 ++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/withings/application_credentials.py b/homeassistant/components/withings/application_credentials.py index ce96ed782dd..0939f9c5b82 100644 --- a/homeassistant/components/withings/application_credentials.py +++ b/homeassistant/components/withings/application_credentials.py @@ -75,3 +75,11 @@ class WithingsLocalOAuth2Implementation(AuthImplementation): } ) return {**token, **new_token} + + +async def async_get_description_placeholders(hass: HomeAssistant) -> dict[str, str]: + """Return description placeholders for the credentials dialog.""" + return { + "developer_dashboard_url": "https://developer.withings.com/dashboard/welcome", + "redirect_url": "https://my.home-assistant.io/redirect/oauth", + } diff --git a/homeassistant/components/withings/strings.json b/homeassistant/components/withings/strings.json index 8eb4293c637..14c7bf640e9 100644 --- a/homeassistant/components/withings/strings.json +++ b/homeassistant/components/withings/strings.json @@ -1,4 +1,7 @@ { + "application_credentials": { + "description": "To be able to login to Withings we require a client ID and secret. To acquire them, please follow the following steps.\n\n1. Go to the [Withings Developer Dashboard]({developer_dashboard_url}) and be sure to select the Public Cloud.\n1. Log in with your Withings account.\n1. Select **Create an application**.\n1. Select the checkbox for **Public API integration**.\n1. Select **Development** as target environment.\n1. Fill in an application name and description of your choice.\n1. Fill in `{redirect_url}` for the registered URL. Make sure that you don't press the button to test it.\n1. Fill in the client ID and secret that are now available." + }, "config": { "step": { "pick_implementation": { @@ -9,7 +12,7 @@ "description": "The Withings integration needs to re-authenticate your account" }, "oauth_discovery": { - "description": "Home Assistant has found a Withings device on your network. Press **Submit** to continue setting up Withings." + "description": "Home Assistant has found a Withings device on your network. Be aware that the setup of Withings is more complicated than many other integrations. Press **Submit** to continue setting up Withings." } }, "error": { From 91e29a3bf125029f215fd51a7a87d5ce3a80e3ef Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Thu, 5 Jun 2025 21:50:19 +0200 Subject: [PATCH 116/266] Bump aioimmich to 0.9.1 (#146222) bump aioimmich to 0.9.1 --- homeassistant/components/immich/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/immich/manifest.json b/homeassistant/components/immich/manifest.json index 1a4ccc8580c..36c993e9c8f 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.9.0"] + "requirements": ["aioimmich==0.9.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index af40384991e..a4b6a12111e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -280,7 +280,7 @@ aiohue==4.7.4 aioimaplib==2.0.1 # homeassistant.components.immich -aioimmich==0.9.0 +aioimmich==0.9.1 # homeassistant.components.apache_kafka aiokafka==0.10.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b5c4c9e7760..1def9ca9e8b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -265,7 +265,7 @@ aiohue==4.7.4 aioimaplib==2.0.1 # homeassistant.components.immich -aioimmich==0.9.0 +aioimmich==0.9.1 # homeassistant.components.apache_kafka aiokafka==0.10.0 From 4d3145e559aa307e4126de2b16d73aeeec88c1a2 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Sat, 7 Jun 2025 12:43:16 +1000 Subject: [PATCH 117/266] Add missing write state to Teslemetry (#146267) --- homeassistant/components/teslemetry/cover.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/teslemetry/cover.py b/homeassistant/components/teslemetry/cover.py index c58559ab308..f6ff71ab0cc 100644 --- a/homeassistant/components/teslemetry/cover.py +++ b/homeassistant/components/teslemetry/cover.py @@ -441,6 +441,7 @@ class TeslemetryStreamingRearTrunkEntity( """Update the entity attributes.""" self._attr_is_closed = None if value is None else not value + self.async_write_ha_state() class TeslemetrySunroofEntity(TeslemetryVehiclePollingEntity, CoverEntity): From 761c2578fba0022af5626fb26c009308c5d97836 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 Jun 2025 09:30:43 -0500 Subject: [PATCH 118/266] Bump aiohttp-fast-zlib to 0.3.0 (#146285) changelog: https://github.com/Bluetooth-Devices/aiohttp-fast-zlib/compare/v0.2.3...v0.3.0 proper aiohttp 3.12 support --- 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 9b5a389e05b..eaa3d025a5b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -5,7 +5,7 @@ aiodiscover==2.7.0 aiodns==3.4.0 aiohasupervisor==0.3.1 aiohttp-asyncmdnsresolver==0.1.1 -aiohttp-fast-zlib==0.2.3 +aiohttp-fast-zlib==0.3.0 aiohttp==3.12.9 aiohttp_cors==0.7.0 aiousbwatcher==1.1.1 diff --git a/pyproject.toml b/pyproject.toml index 2e0d06707dc..48d8265abab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ dependencies = [ "aiohasupervisor==0.3.1", "aiohttp==3.12.9", "aiohttp_cors==0.7.0", - "aiohttp-fast-zlib==0.2.3", + "aiohttp-fast-zlib==0.3.0", "aiohttp-asyncmdnsresolver==0.1.1", "aiozoneinfo==0.2.3", "annotatedyaml==0.4.5", diff --git a/requirements.txt b/requirements.txt index 8c65e9a0b5a..c8c65b783ad 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ aiodns==3.4.0 aiohasupervisor==0.3.1 aiohttp==3.12.9 aiohttp_cors==0.7.0 -aiohttp-fast-zlib==0.2.3 +aiohttp-fast-zlib==0.3.0 aiohttp-asyncmdnsresolver==0.1.1 aiozoneinfo==0.2.3 annotatedyaml==0.4.5 From 79daeb23a99f8b8def5a1fb942b9ac7f9ac509af Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 7 Jun 2025 19:18:24 +0200 Subject: [PATCH 119/266] Bump holidays to 0.74 (#146290) --- homeassistant/components/holiday/manifest.json | 2 +- homeassistant/components/workday/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/holiday/manifest.json b/homeassistant/components/holiday/manifest.json index bd6fd51e726..5a5f1daf967 100644 --- a/homeassistant/components/holiday/manifest.json +++ b/homeassistant/components/holiday/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/holiday", "iot_class": "local_polling", - "requirements": ["holidays==0.73", "babel==2.15.0"] + "requirements": ["holidays==0.74", "babel==2.15.0"] } diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index 7a03133dd86..9091dd131dd 100644 --- a/homeassistant/components/workday/manifest.json +++ b/homeassistant/components/workday/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_polling", "loggers": ["holidays"], "quality_scale": "internal", - "requirements": ["holidays==0.73"] + "requirements": ["holidays==0.74"] } diff --git a/requirements_all.txt b/requirements_all.txt index a4b6a12111e..2742d46bd4d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1161,7 +1161,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.73 +holidays==0.74 # homeassistant.components.frontend home-assistant-frontend==20250531.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1def9ca9e8b..1d12f4f7993 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1007,7 +1007,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.73 +holidays==0.74 # homeassistant.components.frontend home-assistant-frontend==20250531.0 From 21833e7c3128abd134a07b37889e7294d57320c9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 Jun 2025 12:08:32 -0500 Subject: [PATCH 120/266] Bump aiohttp to 3.12.11 (#146298) --- 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 eaa3d025a5b..a994e1d6333 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -6,7 +6,7 @@ aiodns==3.4.0 aiohasupervisor==0.3.1 aiohttp-asyncmdnsresolver==0.1.1 aiohttp-fast-zlib==0.3.0 -aiohttp==3.12.9 +aiohttp==3.12.11 aiohttp_cors==0.7.0 aiousbwatcher==1.1.1 aiozoneinfo==0.2.3 diff --git a/pyproject.toml b/pyproject.toml index 48d8265abab..7f458bebf39 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.9", + "aiohttp==3.12.11", "aiohttp_cors==0.7.0", "aiohttp-fast-zlib==0.3.0", "aiohttp-asyncmdnsresolver==0.1.1", diff --git a/requirements.txt b/requirements.txt index c8c65b783ad..88576cd0c4c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ # Home Assistant Core aiodns==3.4.0 aiohasupervisor==0.3.1 -aiohttp==3.12.9 +aiohttp==3.12.11 aiohttp_cors==0.7.0 aiohttp-fast-zlib==0.3.0 aiohttp-asyncmdnsresolver==0.1.1 From 1fc05d1a307c0a67ba48a45a391d59be5e51394c Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Sun, 8 Jun 2025 19:47:00 +0200 Subject: [PATCH 121/266] Do not probe linkplay device if another config entry already contains the host (#146305) * Do not probe if config entry already contains the host * Add unit test * Use common fixture --- .../components/linkplay/config_flow.py | 3 +++ tests/components/linkplay/test_config_flow.py | 25 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/homeassistant/components/linkplay/config_flow.py b/homeassistant/components/linkplay/config_flow.py index 11e4aabf257..266d2fef857 100644 --- a/homeassistant/components/linkplay/config_flow.py +++ b/homeassistant/components/linkplay/config_flow.py @@ -31,6 +31,9 @@ class LinkPlayConfigFlow(ConfigFlow, domain=DOMAIN): ) -> ConfigFlowResult: """Handle Zeroconf discovery.""" + # Do not probe the device if the host is already configured + self._async_abort_entries_match({CONF_HOST: discovery_info.host}) + session: ClientSession = await async_get_client_session(self.hass) bridge: LinkPlayBridge | None = None diff --git a/tests/components/linkplay/test_config_flow.py b/tests/components/linkplay/test_config_flow.py index adf6aa601ae..8c0dd4af88b 100644 --- a/tests/components/linkplay/test_config_flow.py +++ b/tests/components/linkplay/test_config_flow.py @@ -220,3 +220,28 @@ async def test_user_flow_errors( CONF_HOST: HOST, } assert result["result"].unique_id == UUID + + +@pytest.mark.usefixtures("mock_linkplay_factory_bridge") +async def test_zeroconf_no_probe_existing_device( + hass: HomeAssistant, mock_linkplay_factory_bridge: AsyncMock +) -> None: + """Test we do not probe the device is the host is already configured.""" + entry = MockConfigEntry( + data={CONF_HOST: HOST}, + domain=DOMAIN, + title=NAME, + unique_id=UUID, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=ZEROCONF_DISCOVERY, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + assert len(mock_linkplay_factory_bridge.mock_calls) == 0 From 5e5431c9f9a70dc1e3a44dc24fffb8440538e1fd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 10 Jun 2025 04:48:11 -0500 Subject: [PATCH 122/266] Use entity unique id for ESPHome media player formats (#146318) --- homeassistant/components/esphome/entity.py | 1 + .../components/esphome/media_player.py | 7 +- tests/components/esphome/test_media_player.py | 102 ++++++++++++++++++ 3 files changed, 106 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/esphome/entity.py b/homeassistant/components/esphome/entity.py index 15ea54422d4..37f8e738aee 100644 --- a/homeassistant/components/esphome/entity.py +++ b/homeassistant/components/esphome/entity.py @@ -226,6 +226,7 @@ class EsphomeEntity(EsphomeBaseEntity, Generic[_InfoT, _StateT]): _static_info: _InfoT _state: _StateT _has_state: bool + unique_id: str def __init__( self, diff --git a/homeassistant/components/esphome/media_player.py b/homeassistant/components/esphome/media_player.py index 3af6c0b2049..f18b5e7bf5c 100644 --- a/homeassistant/components/esphome/media_player.py +++ b/homeassistant/components/esphome/media_player.py @@ -78,7 +78,7 @@ class EsphomeMediaPlayer( if self._static_info.supports_pause: flags |= MediaPlayerEntityFeature.PAUSE | MediaPlayerEntityFeature.PLAY self._attr_supported_features = flags - self._entry_data.media_player_formats[static_info.unique_id] = cast( + self._entry_data.media_player_formats[self.unique_id] = cast( MediaPlayerInfo, static_info ).supported_formats @@ -114,9 +114,8 @@ class EsphomeMediaPlayer( media_id = async_process_play_media_url(self.hass, media_id) announcement = kwargs.get(ATTR_MEDIA_ANNOUNCE) bypass_proxy = kwargs.get(ATTR_MEDIA_EXTRA, {}).get(ATTR_BYPASS_PROXY) - supported_formats: list[MediaPlayerSupportedFormat] | None = ( - self._entry_data.media_player_formats.get(self._static_info.unique_id) + self._entry_data.media_player_formats.get(self.unique_id) ) if ( @@ -139,7 +138,7 @@ class EsphomeMediaPlayer( async def async_will_remove_from_hass(self) -> None: """Handle entity being removed.""" await super().async_will_remove_from_hass() - self._entry_data.media_player_formats.pop(self.entity_id, None) + self._entry_data.media_player_formats.pop(self.unique_id, None) def _get_proxy_url( self, diff --git a/tests/components/esphome/test_media_player.py b/tests/components/esphome/test_media_player.py index 18a997dc09a..3f1e5e99c34 100644 --- a/tests/components/esphome/test_media_player.py +++ b/tests/components/esphome/test_media_player.py @@ -430,3 +430,105 @@ async def test_media_player_proxy( mock_async_create_proxy_url.assert_not_called() media_args = mock_client.media_player_command.call_args.kwargs assert media_args["media_url"] == media_url + + +async def test_media_player_formats_reload_preserves_data( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: MockESPHomeDeviceType, +) -> None: + """Test that media player formats are properly managed on reload.""" + # Create a media player with supported formats + supported_formats = [ + MediaPlayerSupportedFormat( + format="mp3", + sample_rate=48000, + num_channels=2, + purpose=MediaPlayerFormatPurpose.DEFAULT, + ), + MediaPlayerSupportedFormat( + format="wav", + sample_rate=16000, + num_channels=1, + purpose=MediaPlayerFormatPurpose.ANNOUNCEMENT, + sample_bytes=2, + ), + ] + + mock_device = await mock_esphome_device( + mock_client=mock_client, + entity_info=[ + MediaPlayerInfo( + object_id="test_media_player", + key=1, + name="Test Media Player", + unique_id="test_unique_id", + supports_pause=True, + supported_formats=supported_formats, + ) + ], + states=[ + MediaPlayerEntityState( + key=1, volume=50, muted=False, state=MediaPlayerState.IDLE + ) + ], + ) + await hass.async_block_till_done() + + # Verify entity was created + state = hass.states.get("media_player.test_test_media_player") + assert state is not None + assert state.state == "idle" + + # Test that play_media works with proxy URL (which requires formats to be stored) + media_url = "http://127.0.0.1/test.mp3" + + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: "media_player.test_test_media_player", + ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, + ATTR_MEDIA_CONTENT_ID: media_url, + }, + blocking=True, + ) + + # Verify the API was called with a proxy URL (contains /api/esphome/ffmpeg_proxy/) + mock_client.media_player_command.assert_called_once() + call_args = mock_client.media_player_command.call_args + assert "/api/esphome/ffmpeg_proxy/" in call_args.kwargs["media_url"] + assert ".mp3" in call_args.kwargs["media_url"] # Should use mp3 format for default + assert call_args.kwargs["announcement"] is None + + mock_client.media_player_command.reset_mock() + + # Reload the integration + await hass.config_entries.async_reload(mock_device.entry.entry_id) + await hass.async_block_till_done() + + # Verify entity still exists after reload + state = hass.states.get("media_player.test_test_media_player") + assert state is not None + + # Test that play_media still works after reload with announcement + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: "media_player.test_test_media_player", + ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, + ATTR_MEDIA_CONTENT_ID: media_url, + ATTR_MEDIA_ANNOUNCE: True, + }, + blocking=True, + ) + + # Verify the API was called with a proxy URL using wav format for announcements + mock_client.media_player_command.assert_called_once() + call_args = mock_client.media_player_command.call_args + assert "/api/esphome/ffmpeg_proxy/" in call_args.kwargs["media_url"] + assert ( + ".wav" in call_args.kwargs["media_url"] + ) # Should use wav format for announcement + assert call_args.kwargs["announcement"] is True From 79919774437dacbfb84d86de1a21831c5b264390 Mon Sep 17 00:00:00 2001 From: Sanjay Govind Date: Mon, 9 Jun 2025 00:35:54 +1200 Subject: [PATCH 123/266] Fix bosch alarm areas not correctly subscribing to alarms (#146322) * Fix bosch alarm areas not correctly subscribing to alarms * add test --- .../components/bosch_alarm/alarm_control_panel.py | 2 +- .../components/bosch_alarm/test_alarm_control_panel.py | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bosch_alarm/alarm_control_panel.py b/homeassistant/components/bosch_alarm/alarm_control_panel.py index 60365070587..b502ee32fca 100644 --- a/homeassistant/components/bosch_alarm/alarm_control_panel.py +++ b/homeassistant/components/bosch_alarm/alarm_control_panel.py @@ -50,7 +50,7 @@ class AreaAlarmControlPanel(BoschAlarmAreaEntity, AlarmControlPanelEntity): def __init__(self, panel: Panel, area_id: int, unique_id: str) -> None: """Initialise a Bosch Alarm control panel entity.""" - super().__init__(panel, area_id, unique_id, False, False, True) + super().__init__(panel, area_id, unique_id, True, False, True) self._attr_unique_id = self._area_unique_id @property diff --git a/tests/components/bosch_alarm/test_alarm_control_panel.py b/tests/components/bosch_alarm/test_alarm_control_panel.py index 31d2f928ec5..51767396880 100644 --- a/tests/components/bosch_alarm/test_alarm_control_panel.py +++ b/tests/components/bosch_alarm/test_alarm_control_panel.py @@ -66,6 +66,16 @@ async def test_update_alarm_device( assert hass.states.get(entity_id).state == AlarmControlPanelState.ARMED_AWAY + area.is_triggered.return_value = True + + await call_observable(hass, area.alarm_observer) + + assert hass.states.get(entity_id).state == AlarmControlPanelState.TRIGGERED + + area.is_triggered.return_value = False + + await call_observable(hass, area.alarm_observer) + await hass.services.async_call( ALARM_CONTROL_PANEL_DOMAIN, SERVICE_ALARM_DISARM, From 0eb3714abcdd4bd9c2b80960885318625da256bf Mon Sep 17 00:00:00 2001 From: tronikos Date: Sun, 8 Jun 2025 11:47:46 -0700 Subject: [PATCH 124/266] Allow different manufacturer than Amazon in Amazon Devices (#146333) --- homeassistant/components/amazon_devices/entity.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/amazon_devices/entity.py b/homeassistant/components/amazon_devices/entity.py index bab8009ceb0..962e2f55ae6 100644 --- a/homeassistant/components/amazon_devices/entity.py +++ b/homeassistant/components/amazon_devices/entity.py @@ -25,15 +25,15 @@ class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]): """Initialize the entity.""" super().__init__(coordinator) self._serial_num = serial_num - model_details = coordinator.api.get_model_details(self.device) - model = model_details["model"] if model_details else None + model_details = coordinator.api.get_model_details(self.device) or {} + model = model_details.get("model") self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, serial_num)}, name=self.device.account_name, model=model, model_id=self.device.device_type, - manufacturer="Amazon", - hw_version=model_details["hw_version"] if model_details else None, + manufacturer=model_details.get("manufacturer", "Amazon"), + hw_version=model_details.get("hw_version"), sw_version=( self.device.software_version if model != SPEAKER_GROUP_MODEL else None ), From 80b09e3212034d9e923014da2a1738fe2af82ac2 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 8 Jun 2025 18:02:06 +0200 Subject: [PATCH 125/266] Bump py-synologydsm-api to 2.7.3 (#146338) bump py-synologydsm-api to 2.7.3 --- homeassistant/components/synology_dsm/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/synology_dsm/manifest.json b/homeassistant/components/synology_dsm/manifest.json index cd054c7eb74..3022b4c2af9 100644 --- a/homeassistant/components/synology_dsm/manifest.json +++ b/homeassistant/components/synology_dsm/manifest.json @@ -7,7 +7,7 @@ "documentation": "https://www.home-assistant.io/integrations/synology_dsm", "iot_class": "local_polling", "loggers": ["synology_dsm"], - "requirements": ["py-synologydsm-api==2.7.2"], + "requirements": ["py-synologydsm-api==2.7.3"], "ssdp": [ { "manufacturer": "Synology", diff --git a/requirements_all.txt b/requirements_all.txt index 2742d46bd4d..fb2a9172301 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1774,7 +1774,7 @@ py-schluter==0.1.7 py-sucks==0.9.11 # homeassistant.components.synology_dsm -py-synologydsm-api==2.7.2 +py-synologydsm-api==2.7.3 # homeassistant.components.atome pyAtome==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1d12f4f7993..e056dfe6700 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1494,7 +1494,7 @@ py-nightscout==1.2.2 py-sucks==0.9.11 # homeassistant.components.synology_dsm -py-synologydsm-api==2.7.2 +py-synologydsm-api==2.7.3 # homeassistant.components.hdmi_cec pyCEC==0.5.2 From b3ee2a8885069da550dbe4e677afb6d063f2e1dd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 8 Jun 2025 11:15:00 -0500 Subject: [PATCH 126/266] Bump aioesphomeapi to 32.2.0 (#146344) --- 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 eea0ed060f9..3ae66838823 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==32.0.0", + "aioesphomeapi==32.2.0", "esphome-dashboard-api==1.3.0", "bleak-esphome==2.16.0" ], diff --git a/requirements_all.txt b/requirements_all.txt index fb2a9172301..fbcffd47dee 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -244,7 +244,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==32.0.0 +aioesphomeapi==32.2.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e056dfe6700..df13b85db26 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -232,7 +232,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==32.0.0 +aioesphomeapi==32.2.0 # homeassistant.components.flo aioflo==2021.11.0 From e97ab1fe3cc4b5e32652be88f9c77bf68ae2cea2 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Tue, 10 Jun 2025 14:38:52 +0200 Subject: [PATCH 127/266] Change interval for Powerfox integration (#146348) --- homeassistant/components/powerfox/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/powerfox/const.py b/homeassistant/components/powerfox/const.py index 0970e8a1b66..790f241ae8e 100644 --- a/homeassistant/components/powerfox/const.py +++ b/homeassistant/components/powerfox/const.py @@ -8,4 +8,4 @@ from typing import Final DOMAIN: Final = "powerfox" LOGGER = logging.getLogger(__package__) -SCAN_INTERVAL = timedelta(minutes=1) +SCAN_INTERVAL = timedelta(seconds=10) From bfe2eeb8332e7b1da5ed7922f6fd88e7dd2a9ee2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 10 Jun 2025 08:30:19 -0500 Subject: [PATCH 128/266] Shift ESPHome log parsing to the library (#146349) --- homeassistant/components/esphome/manager.py | 23 +++++++++------------ 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index 1b0e4fc8986..b4af39586d4 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -5,7 +5,6 @@ from __future__ import annotations import asyncio from functools import partial import logging -import re from typing import TYPE_CHECKING, Any, NamedTuple from aioesphomeapi import ( @@ -23,6 +22,7 @@ from aioesphomeapi import ( RequiresEncryptionAPIError, UserService, UserServiceArgType, + parse_log_message, ) from awesomeversion import AwesomeVersion import voluptuous as vol @@ -110,11 +110,6 @@ LOGGER_TO_LOG_LEVEL = { logging.ERROR: LogLevel.LOG_LEVEL_ERROR, logging.CRITICAL: LogLevel.LOG_LEVEL_ERROR, } -# 7-bit and 8-bit C1 ANSI sequences -# https://stackoverflow.com/questions/14693701/how-can-i-remove-the-ansi-escape-sequences-from-a-string-in-python -ANSI_ESCAPE_78BIT = re.compile( - rb"(?:\x1B[@-Z\\-_]|[\x80-\x9A\x9C-\x9F]|(?:\x1B\[|\x9B)[0-?]*[ -/]*[@-~])" -) @callback @@ -387,13 +382,15 @@ class ESPHomeManager: def _async_on_log(self, msg: SubscribeLogsResponse) -> None: """Handle a log message from the API.""" - log: bytes = msg.message - _LOGGER.log( - LOG_LEVEL_TO_LOGGER.get(msg.level, logging.DEBUG), - "%s: %s", - self.entry.title, - ANSI_ESCAPE_78BIT.sub(b"", log).decode("utf-8", "backslashreplace"), - ) + for line in parse_log_message( + msg.message.decode("utf-8", "backslashreplace"), "", strip_ansi_escapes=True + ): + _LOGGER.log( + LOG_LEVEL_TO_LOGGER.get(msg.level, logging.DEBUG), + "%s: %s", + self.entry.title, + line, + ) @callback def _async_get_equivalent_log_level(self) -> LogLevel: From 7bd6ec68a888f60d95f2fb3c7c93e79a343c553e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Diego=20Rodr=C3=ADguez=20Royo?= Date: Tue, 10 Jun 2025 11:41:36 +0200 Subject: [PATCH 129/266] Explain Home Connect setup (#146356) * Explain Home Connect setup * Avoid using "we" * Fix login spelling * Fix signup spelling --- .../components/home_connect/application_credentials.py | 10 ++++++++++ homeassistant/components/home_connect/strings.json | 5 ++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/home_connect/application_credentials.py b/homeassistant/components/home_connect/application_credentials.py index d66255e6810..20a3a211b6a 100644 --- a/homeassistant/components/home_connect/application_credentials.py +++ b/homeassistant/components/home_connect/application_credentials.py @@ -12,3 +12,13 @@ async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationSe authorize_url=OAUTH2_AUTHORIZE, token_url=OAUTH2_TOKEN, ) + + +async def async_get_description_placeholders(hass: HomeAssistant) -> dict[str, str]: + """Return description placeholders for the credentials dialog.""" + return { + "developer_dashboard_url": "https://developer.home-connect.com/", + "applications_url": "https://developer.home-connect.com/applications", + "register_application_url": "https://developer.home-connect.com/application/add", + "redirect_url": "https://my.home-assistant.io/redirect/oauth", + } diff --git a/homeassistant/components/home_connect/strings.json b/homeassistant/components/home_connect/strings.json index 9d33f1d3ffd..71a1f1918f6 100644 --- a/homeassistant/components/home_connect/strings.json +++ b/homeassistant/components/home_connect/strings.json @@ -1,4 +1,7 @@ { + "application_credentials": { + "description": "Login to Home Connect requires a client ID and secret. To acquire them, please follow the following steps.\n\n1. Visit the [Home Connect Developer Program website]({developer_dashboard_url}) and signup for a development account.\n1. Enter the email of your login for the original Home Connect app under **Default Home Connect User Account for Testing** in the sign up process.\n1. Go to the [Applications]({applications_url}) page and select [Register Application]({register_application_url}) and set the fields to the following values: \n\t* **Application ID**: Home Assistant (or any other name that makes sense)\n\t* **OAuth Flow**: Authorization Code Grant Flow\n\t* **Redirect URI**: `{redirect_url}`\n\nIn the newly created application's details, you will find the **Client ID** and the **Client Secret**." + }, "common": { "confirmed": "Confirmed", "present": "Present" @@ -13,7 +16,7 @@ "description": "The Home Connect integration needs to re-authenticate your account" }, "oauth_discovery": { - "description": "Home Assistant has found a Home Connect device on your network. Press **Submit** to continue setting up Home Connect." + "description": "Home Assistant has found a Home Connect device on your network. Be aware that the setup of Home Connect is more complicated than many other integrations. Press **Submit** to continue setting up Home Connect." } }, "abort": { From d89b99f42b08966512b2c8b9912f7d1477f77ae0 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Tue, 10 Jun 2025 14:10:49 +0200 Subject: [PATCH 130/266] Improve error logging in trend binary sensor (#146358) --- .../components/trend/binary_sensor.py | 9 +++- tests/components/trend/test_binary_sensor.py | 47 +++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/trend/binary_sensor.py b/homeassistant/components/trend/binary_sensor.py index 4261f96bbe6..2bc5949b970 100644 --- a/homeassistant/components/trend/binary_sensor.py +++ b/homeassistant/components/trend/binary_sensor.py @@ -239,7 +239,14 @@ class SensorTrend(BinarySensorEntity, RestoreEntity): self.async_schedule_update_ha_state(True) except (ValueError, TypeError) as ex: - _LOGGER.error(ex) + _LOGGER.error( + "Error processing sensor state change for " + "entity_id=%s, attribute=%s, state=%s: %s", + self._entity_id, + self._attribute, + new_state.state, + ex, + ) self.async_on_remove( async_track_state_change_event( diff --git a/tests/components/trend/test_binary_sensor.py b/tests/components/trend/test_binary_sensor.py index 4a829bb86d2..4f19c7e3427 100644 --- a/tests/components/trend/test_binary_sensor.py +++ b/tests/components/trend/test_binary_sensor.py @@ -437,3 +437,50 @@ async def test_unavailable_source( await hass.async_block_till_done() assert hass.states.get("binary_sensor.test_trend_sensor").state == "on" + + +async def test_invalid_state_handling( + hass: HomeAssistant, + config_entry: MockConfigEntry, + setup_component: ComponentSetup, + freezer: FrozenDateTimeFactory, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test handling of invalid states in trend sensor.""" + await setup_component( + { + "sample_duration": 10000, + "min_gradient": 1, + "max_samples": 25, + "min_samples": 5, + }, + ) + + for val in (10, 20, 30, 40, 50, 60): + freezer.tick(timedelta(seconds=2)) + hass.states.async_set("sensor.test_state", val) + await hass.async_block_till_done() + + assert hass.states.get("binary_sensor.test_trend_sensor").state == STATE_ON + + # Set an invalid state + hass.states.async_set("sensor.test_state", "invalid") + await hass.async_block_till_done() + + # The trend sensor should handle the invalid state gracefully + assert (sensor_state := hass.states.get("binary_sensor.test_trend_sensor")) + assert sensor_state.state == STATE_ON + + # Check if a warning is logged + assert ( + "Error processing sensor state change for entity_id=sensor.test_state, " + "attribute=None, state=invalid: could not convert string to float: 'invalid'" + ) in caplog.text + + # Set a valid state again + hass.states.async_set("sensor.test_state", 50) + await hass.async_block_till_done() + + # The trend sensor should return to a valid state + assert (sensor_state := hass.states.get("binary_sensor.test_trend_sensor")) + assert sensor_state.state == "on" From 0874f1c350741becb68e7acee552f33e29b0ee19 Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Sun, 8 Jun 2025 23:43:20 +0200 Subject: [PATCH 131/266] Bump python-linkplay to v0.2.10 (#146359) --- homeassistant/components/linkplay/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/linkplay/manifest.json b/homeassistant/components/linkplay/manifest.json index eb9b5a87c75..1bbf70ed3ac 100644 --- a/homeassistant/components/linkplay/manifest.json +++ b/homeassistant/components/linkplay/manifest.json @@ -7,6 +7,6 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["linkplay"], - "requirements": ["python-linkplay==0.2.9"], + "requirements": ["python-linkplay==0.2.10"], "zeroconf": ["_linkplay._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index fbcffd47dee..19a71af3641 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2452,7 +2452,7 @@ python-juicenet==1.1.0 python-kasa[speedups]==0.10.2 # homeassistant.components.linkplay -python-linkplay==0.2.9 +python-linkplay==0.2.10 # homeassistant.components.lirc # python-lirc==1.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index df13b85db26..befaeeebbe3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2022,7 +2022,7 @@ python-juicenet==1.1.0 python-kasa[speedups]==0.10.2 # homeassistant.components.linkplay -python-linkplay==0.2.9 +python-linkplay==0.2.10 # homeassistant.components.lirc # python-lirc==1.2.3 From ca77b5210f5b88a3e56c41de4fcaac895bebe6a8 Mon Sep 17 00:00:00 2001 From: David Knowles Date: Mon, 9 Jun 2025 14:00:37 -0400 Subject: [PATCH 132/266] Bump pydrawise to 2025.6.0 (#146369) --- homeassistant/components/hydrawise/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hydrawise/manifest.json b/homeassistant/components/hydrawise/manifest.json index 0c355c34a71..03b9dc68a79 100644 --- a/homeassistant/components/hydrawise/manifest.json +++ b/homeassistant/components/hydrawise/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/hydrawise", "iot_class": "cloud_polling", "loggers": ["pydrawise"], - "requirements": ["pydrawise==2025.3.0"] + "requirements": ["pydrawise==2025.6.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 19a71af3641..f2778052b4e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1925,7 +1925,7 @@ pydiscovergy==3.0.2 pydoods==1.0.2 # homeassistant.components.hydrawise -pydrawise==2025.3.0 +pydrawise==2025.6.0 # homeassistant.components.android_ip_webcam pydroid-ipcam==3.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index befaeeebbe3..8ea955cb20e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1603,7 +1603,7 @@ pydexcom==0.2.3 pydiscovergy==3.0.2 # homeassistant.components.hydrawise -pydrawise==2025.3.0 +pydrawise==2025.6.0 # homeassistant.components.android_ip_webcam pydroid-ipcam==3.0.0 From 0b24a9abc30c39128d88ac57c325c61ea1014884 Mon Sep 17 00:00:00 2001 From: Michael Davie Date: Mon, 9 Jun 2025 13:53:44 -0400 Subject: [PATCH 133/266] Bump env-canada to v0.11.2 (#146371) --- homeassistant/components/environment_canada/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index da0be245fcd..a6a6e447426 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/environment_canada", "iot_class": "cloud_polling", "loggers": ["env_canada"], - "requirements": ["env-canada==0.10.2"] + "requirements": ["env-canada==0.11.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index f2778052b4e..ef173c1eb9d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -878,7 +878,7 @@ enocean==0.50 enturclient==0.2.4 # homeassistant.components.environment_canada -env-canada==0.10.2 +env-canada==0.11.2 # homeassistant.components.season ephem==4.1.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8ea955cb20e..9bb4a42d641 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -757,7 +757,7 @@ energyzero==2.1.1 enocean==0.50 # homeassistant.components.environment_canada -env-canada==0.10.2 +env-canada==0.11.2 # homeassistant.components.season ephem==4.1.6 From e7a7b2417b874d206cf0fd5cdd7924a35cc9907c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 9 Jun 2025 19:25:29 -0500 Subject: [PATCH 134/266] Bump aioesphomeapi to 32.2.1 (#146375) --- 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 3ae66838823..9b70aba4de1 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==32.2.0", + "aioesphomeapi==32.2.1", "esphome-dashboard-api==1.3.0", "bleak-esphome==2.16.0" ], diff --git a/requirements_all.txt b/requirements_all.txt index ef173c1eb9d..88e5ab99bc4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -244,7 +244,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==32.2.0 +aioesphomeapi==32.2.1 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9bb4a42d641..c1af6b4f120 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -232,7 +232,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==32.2.0 +aioesphomeapi==32.2.1 # homeassistant.components.flo aioflo==2021.11.0 From f629731930598fd336e3a8cf24b95430fc2b1952 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 9 Jun 2025 20:59:02 +0300 Subject: [PATCH 135/266] Bump aioamazondevices to 3.0.6 (#146385) --- homeassistant/components/amazon_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/amazon_devices/manifest.json b/homeassistant/components/amazon_devices/manifest.json index bd9bc701d3e..37a56486a08 100644 --- a/homeassistant/components/amazon_devices/manifest.json +++ b/homeassistant/components/amazon_devices/manifest.json @@ -118,5 +118,5 @@ "iot_class": "cloud_polling", "loggers": ["aioamazondevices"], "quality_scale": "bronze", - "requirements": ["aioamazondevices==3.0.5"] + "requirements": ["aioamazondevices==3.0.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index 88e5ab99bc4..946f5bcfcc2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -182,7 +182,7 @@ aioairzone-cloud==0.6.12 aioairzone==1.0.0 # homeassistant.components.amazon_devices -aioamazondevices==3.0.5 +aioamazondevices==3.0.6 # homeassistant.components.ambient_network # homeassistant.components.ambient_station diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c1af6b4f120..ff1899391c1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -170,7 +170,7 @@ aioairzone-cloud==0.6.12 aioairzone==1.0.0 # homeassistant.components.amazon_devices -aioamazondevices==3.0.5 +aioamazondevices==3.0.6 # homeassistant.components.ambient_network # homeassistant.components.ambient_station From 3d0d70ece65e537be359ce8a2243164721c998d0 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 9 Jun 2025 13:24:40 +0200 Subject: [PATCH 136/266] Fix switch_as_x entity_id tracking (#146386) --- .../components/switch_as_x/__init__.py | 21 +++++-- tests/components/switch_as_x/test_init.py | 63 +++++++++++++------ 2 files changed, 58 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/switch_as_x/__init__.py b/homeassistant/components/switch_as_x/__init__.py index 71cb9e9c225..b07bf0fdaec 100644 --- a/homeassistant/components/switch_as_x/__init__.py +++ b/homeassistant/components/switch_as_x/__init__.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.components.homeassistant import exposed_entities from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ENTITY_ID -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback, valid_entity_id from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.event import async_track_entity_registry_updated_event @@ -44,10 +44,12 @@ def async_add_to_device( async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - registry = er.async_get(hass) + entity_registry = er.async_get(hass) device_registry = dr.async_get(hass) try: - entity_id = er.async_validate_entity_id(registry, entry.options[CONF_ENTITY_ID]) + entity_id = er.async_validate_entity_id( + entity_registry, entry.options[CONF_ENTITY_ID] + ) except vol.Invalid: # The entity is identified by an unknown entity registry ID _LOGGER.error( @@ -68,14 +70,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return if "entity_id" in data["changes"]: - # Entity_id changed, reload the config entry - await hass.config_entries.async_reload(entry.entry_id) + # Entity_id changed, update or reload the config entry + if valid_entity_id(entry.options[CONF_ENTITY_ID]): + # If the entity is pointed to by an entity ID, update the entry + hass.config_entries.async_update_entry( + entry, + options={**entry.options, CONF_ENTITY_ID: data["entity_id"]}, + ) + else: + await hass.config_entries.async_reload(entry.entry_id) if device_id and "device_id" in data["changes"]: # If the tracked switch is no longer in the device, remove our config entry # from the device if ( - not (entity_entry := registry.async_get(data[CONF_ENTITY_ID])) + not (entity_entry := entity_registry.async_get(data[CONF_ENTITY_ID])) or not device_registry.async_get(device_id) or entity_entry.device_id == device_id ): diff --git a/tests/components/switch_as_x/test_init.py b/tests/components/switch_as_x/test_init.py index cd80fab69bc..0b965fc2ad1 100644 --- a/tests/components/switch_as_x/test_init.py +++ b/tests/components/switch_as_x/test_init.py @@ -39,6 +39,44 @@ EXPOSE_SETTINGS = { } +@pytest.fixture +def switch_entity_registry_entry( + entity_registry: er.EntityRegistry, +) -> er.RegistryEntry: + """Fixture to create a switch entity entry.""" + return entity_registry.async_get_or_create( + "switch", "test", "unique", original_name="ABC" + ) + + +@pytest.fixture +def switch_as_x_config_entry( + hass: HomeAssistant, + switch_entity_registry_entry: er.RegistryEntry, + target_domain: str, + use_entity_registry_id: bool, +) -> MockConfigEntry: + """Fixture to create a switch_as_x config entry.""" + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + CONF_ENTITY_ID: switch_entity_registry_entry.id + if use_entity_registry_id + else switch_entity_registry_entry.entity_id, + CONF_INVERT: False, + CONF_TARGET_DOMAIN: target_domain, + }, + title="ABC", + version=SwitchAsXConfigFlowHandler.VERSION, + minor_version=SwitchAsXConfigFlowHandler.MINOR_VERSION, + ) + + config_entry.add_to_hass(hass) + + return config_entry + + @pytest.mark.parametrize("target_domain", PLATFORMS_TO_TEST) async def test_config_entry_unregistered_uuid( hass: HomeAssistant, target_domain: str @@ -67,6 +105,7 @@ async def test_config_entry_unregistered_uuid( assert len(hass.states.async_all()) == 0 +@pytest.mark.parametrize("use_entity_registry_id", [True, False]) @pytest.mark.parametrize( ("target_domain", "state_on", "state_off"), [ @@ -81,33 +120,17 @@ async def test_config_entry_unregistered_uuid( async def test_entity_registry_events( hass: HomeAssistant, entity_registry: er.EntityRegistry, + switch_entity_registry_entry: er.RegistryEntry, + switch_as_x_config_entry: MockConfigEntry, target_domain: str, state_on: str, state_off: str, ) -> None: """Test entity registry events are tracked.""" - registry_entry = entity_registry.async_get_or_create( - "switch", "test", "unique", original_name="ABC" - ) - switch_entity_id = registry_entry.entity_id + switch_entity_id = switch_entity_registry_entry.entity_id hass.states.async_set(switch_entity_id, STATE_ON) - config_entry = MockConfigEntry( - data={}, - domain=DOMAIN, - options={ - CONF_ENTITY_ID: registry_entry.id, - CONF_INVERT: False, - CONF_TARGET_DOMAIN: target_domain, - }, - title="ABC", - version=SwitchAsXConfigFlowHandler.VERSION, - minor_version=SwitchAsXConfigFlowHandler.MINOR_VERSION, - ) - - config_entry.add_to_hass(hass) - - assert await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(switch_as_x_config_entry.entry_id) await hass.async_block_till_done() assert hass.states.get(f"{target_domain}.abc").state == state_on From 218864d08c06844f64a2281662fdbea28fb048cc Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 9 Jun 2025 17:04:55 +0200 Subject: [PATCH 137/266] Update switch_as_x to handle wrapped switch moved to another device (#146387) * Update switch_as_x to handle wrapped switch moved to another device * Reload switch_as_x config entry after updating device * Make sure the switch_as_x entity is not removed --- .../components/switch_as_x/__init__.py | 24 ++- tests/components/switch_as_x/test_init.py | 151 ++++++++++++++++-- 2 files changed, 164 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/switch_as_x/__init__.py b/homeassistant/components/switch_as_x/__init__.py index b07bf0fdaec..9e40a99299a 100644 --- a/homeassistant/components/switch_as_x/__init__.py +++ b/homeassistant/components/switch_as_x/__init__.py @@ -13,7 +13,7 @@ from homeassistant.core import Event, HomeAssistant, callback, valid_entity_id from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.event import async_track_entity_registry_updated_event -from .const import CONF_INVERT, CONF_TARGET_DOMAIN +from .const import CONF_INVERT, CONF_TARGET_DOMAIN, DOMAIN from .light import LightSwitch __all__ = ["LightSwitch"] @@ -81,7 +81,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.config_entries.async_reload(entry.entry_id) if device_id and "device_id" in data["changes"]: - # If the tracked switch is no longer in the device, remove our config entry + # Handle the wrapped switch being moved to a different device or removed # from the device if ( not (entity_entry := entity_registry.async_get(data[CONF_ENTITY_ID])) @@ -91,10 +91,30 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # No need to do any cleanup return + # The wrapped switch has been moved to a different device, update the + # switch_as_x entity and the device entry to include our config entry + switch_as_x_entity_id = entity_registry.async_get_entity_id( + entry.options[CONF_TARGET_DOMAIN], DOMAIN, entry.entry_id + ) + if switch_as_x_entity_id: + # Update the switch_as_x entity to point to the new device (or no device) + entity_registry.async_update_entity( + switch_as_x_entity_id, device_id=entity_entry.device_id + ) + + if entity_entry.device_id is not None: + device_registry.async_update_device( + entity_entry.device_id, add_config_entry_id=entry.entry_id + ) + device_registry.async_update_device( device_id, remove_config_entry_id=entry.entry_id ) + # Reload the config entry so the switch_as_x entity is recreated with + # correct device info + await hass.config_entries.async_reload(entry.entry_id) + entry.async_on_unload( async_track_entity_registry_updated_event( hass, entity_id, async_registry_updated diff --git a/tests/components/switch_as_x/test_init.py b/tests/components/switch_as_x/test_init.py index 0b965fc2ad1..2c87b0e3a92 100644 --- a/tests/components/switch_as_x/test_init.py +++ b/tests/components/switch_as_x/test_init.py @@ -6,6 +6,7 @@ from unittest.mock import patch import pytest +from homeassistant.components import switch_as_x from homeassistant.components.homeassistant import exposed_entities from homeassistant.components.lock import LockState from homeassistant.components.switch_as_x.config_flow import SwitchAsXConfigFlowHandler @@ -24,8 +25,9 @@ from homeassistant.const import ( EntityCategory, Platform, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import Event, HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.event import async_track_entity_registry_updated_event from homeassistant.setup import async_setup_component from . import PLATFORMS_TO_TEST @@ -222,16 +224,39 @@ async def test_device_registry_config_entry_1( device_entry = device_registry.async_get(device_entry.id) assert switch_as_x_config_entry.entry_id in device_entry.config_entries - # Remove the wrapped switch's config entry from the device - device_registry.async_update_device( - device_entry.id, remove_config_entry_id=switch_config_entry.entry_id - ) - await hass.async_block_till_done() - await hass.async_block_till_done() + events = [] + + def add_event(event: Event[er.EventEntityRegistryUpdatedData]) -> None: + """Add entity registry updated event to the list.""" + events.append(event.data["action"]) + + async_track_entity_registry_updated_event(hass, entity_entry.entity_id, add_event) + + # Remove the wrapped switch's config entry from the device, this removes the + # wrapped switch + with patch( + "homeassistant.components.switch_as_x.async_unload_entry", + wraps=switch_as_x.async_unload_entry, + ) as mock_setup_entry: + device_registry.async_update_device( + device_entry.id, remove_config_entry_id=switch_config_entry.entry_id + ) + await hass.async_block_till_done() + await hass.async_block_till_done() + mock_setup_entry.assert_called_once() + # Check that the switch_as_x config entry is removed from the device device_entry = device_registry.async_get(device_entry.id) assert switch_as_x_config_entry.entry_id not in device_entry.config_entries + # Check that the switch_as_x config entry is removed + assert ( + switch_as_x_config_entry.entry_id not in hass.config_entries.async_entry_ids() + ) + + # Check we got the expected events + assert events == ["remove"] + @pytest.mark.parametrize("target_domain", PLATFORMS_TO_TEST) async def test_device_registry_config_entry_2( @@ -281,13 +306,121 @@ async def test_device_registry_config_entry_2( device_entry = device_registry.async_get(device_entry.id) assert switch_as_x_config_entry.entry_id in device_entry.config_entries + events = [] + + def add_event(event: Event[er.EventEntityRegistryUpdatedData]) -> None: + """Add entity registry updated event to the list.""" + events.append(event.data["action"]) + + async_track_entity_registry_updated_event(hass, entity_entry.entity_id, add_event) + # Remove the wrapped switch from the device - entity_registry.async_update_entity(switch_entity_entry.entity_id, device_id=None) - await hass.async_block_till_done() + with patch( + "homeassistant.components.switch_as_x.async_unload_entry", + wraps=switch_as_x.async_unload_entry, + ) as mock_setup_entry: + entity_registry.async_update_entity( + switch_entity_entry.entity_id, device_id=None + ) + await hass.async_block_till_done() + mock_setup_entry.assert_called_once() + # Check that the switch_as_x config entry is removed from the device device_entry = device_registry.async_get(device_entry.id) assert switch_as_x_config_entry.entry_id not in device_entry.config_entries + # Check that the switch_as_x config entry is not removed + assert switch_as_x_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == ["update"] + + +@pytest.mark.parametrize("target_domain", PLATFORMS_TO_TEST) +async def test_device_registry_config_entry_3( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + target_domain: str, +) -> None: + """Test we add our config entry to the tracked switch's device.""" + switch_config_entry = MockConfigEntry() + switch_config_entry.add_to_hass(hass) + + device_entry = device_registry.async_get_or_create( + config_entry_id=switch_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + device_entry_2 = device_registry.async_get_or_create( + config_entry_id=switch_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:FF")}, + ) + switch_entity_entry = entity_registry.async_get_or_create( + "switch", + "test", + "unique", + config_entry=switch_config_entry, + device_id=device_entry.id, + original_name="ABC", + ) + + switch_as_x_config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + CONF_ENTITY_ID: switch_entity_entry.id, + CONF_INVERT: False, + CONF_TARGET_DOMAIN: target_domain, + }, + title="ABC", + version=SwitchAsXConfigFlowHandler.VERSION, + minor_version=SwitchAsXConfigFlowHandler.MINOR_VERSION, + ) + + switch_as_x_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(switch_as_x_config_entry.entry_id) + await hass.async_block_till_done() + + entity_entry = entity_registry.async_get(f"{target_domain}.abc") + assert entity_entry.device_id == switch_entity_entry.device_id + + device_entry = device_registry.async_get(device_entry.id) + assert switch_as_x_config_entry.entry_id in device_entry.config_entries + device_entry_2 = device_registry.async_get(device_entry_2.id) + assert switch_as_x_config_entry.entry_id not in device_entry_2.config_entries + + events = [] + + def add_event(event: Event[er.EventEntityRegistryUpdatedData]) -> None: + """Add entity registry updated event to the list.""" + events.append(event.data["action"]) + + async_track_entity_registry_updated_event(hass, entity_entry.entity_id, add_event) + + # Move the wrapped switch to another device + with patch( + "homeassistant.components.switch_as_x.async_unload_entry", + wraps=switch_as_x.async_unload_entry, + ) as mock_setup_entry: + entity_registry.async_update_entity( + switch_entity_entry.entity_id, device_id=device_entry_2.id + ) + await hass.async_block_till_done() + mock_setup_entry.assert_called_once() + + # Check that the switch_as_x config entry is moved to the other device + device_entry = device_registry.async_get(device_entry.id) + assert switch_as_x_config_entry.entry_id not in device_entry.config_entries + device_entry_2 = device_registry.async_get(device_entry_2.id) + assert switch_as_x_config_entry.entry_id in device_entry_2.config_entries + + # Check that the switch_as_x config entry is not removed + assert switch_as_x_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == ["update"] + @pytest.mark.parametrize("target_domain", PLATFORMS_TO_TEST) async def test_config_entry_entity_id( From a3220ecae6c33c1175d401a0e669cd680dad1346 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 9 Jun 2025 19:51:46 +0200 Subject: [PATCH 138/266] Bump pynordpool to 0.3.0 (#146396) --- homeassistant/components/nordpool/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nordpool/manifest.json b/homeassistant/components/nordpool/manifest.json index b096d2bd506..ca299b470ea 100644 --- a/homeassistant/components/nordpool/manifest.json +++ b/homeassistant/components/nordpool/manifest.json @@ -8,6 +8,6 @@ "iot_class": "cloud_polling", "loggers": ["pynordpool"], "quality_scale": "platinum", - "requirements": ["pynordpool==0.2.4"], + "requirements": ["pynordpool==0.3.0"], "single_config_entry": true } diff --git a/requirements_all.txt b/requirements_all.txt index 946f5bcfcc2..dd84a358fd4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2174,7 +2174,7 @@ pynina==0.3.6 pynobo==1.8.1 # homeassistant.components.nordpool -pynordpool==0.2.4 +pynordpool==0.3.0 # homeassistant.components.nuki pynuki==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ff1899391c1..d0c4252f456 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1804,7 +1804,7 @@ pynina==0.3.6 pynobo==1.8.1 # homeassistant.components.nordpool -pynordpool==0.2.4 +pynordpool==0.3.0 # homeassistant.components.nuki pynuki==1.6.3 From c6ff0e64927d429c7f56e2e7b460442eebfbbe9a Mon Sep 17 00:00:00 2001 From: wittypluck Date: Mon, 9 Jun 2025 19:55:09 +0200 Subject: [PATCH 139/266] Fix CO concentration unit in OpenWeatherMap (#146403) --- homeassistant/components/openweathermap/sensor.py | 3 +-- tests/components/openweathermap/snapshots/test_sensor.ambr | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/openweathermap/sensor.py b/homeassistant/components/openweathermap/sensor.py index 789e9647f77..87b7860afb5 100644 --- a/homeassistant/components/openweathermap/sensor.py +++ b/homeassistant/components/openweathermap/sensor.py @@ -10,7 +10,6 @@ from homeassistant.components.sensor import ( ) from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - CONCENTRATION_PARTS_PER_MILLION, DEGREE, PERCENTAGE, UV_INDEX, @@ -170,7 +169,7 @@ AIRPOLLUTION_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key=ATTR_API_AIRPOLLUTION_CO, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, device_class=SensorDeviceClass.CO, state_class=SensorStateClass.MEASUREMENT, ), diff --git a/tests/components/openweathermap/snapshots/test_sensor.ambr b/tests/components/openweathermap/snapshots/test_sensor.ambr index cbd86f14676..11a1feb721f 100644 --- a/tests/components/openweathermap/snapshots/test_sensor.ambr +++ b/tests/components/openweathermap/snapshots/test_sensor.ambr @@ -86,7 +86,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '12.34-56.78-co', - 'unit_of_measurement': 'ppm', + 'unit_of_measurement': 'µg/m³', }) # --- # name: test_sensor_states[air_pollution][sensor.openweathermap_carbon_monoxide-state] @@ -96,7 +96,7 @@ 'device_class': 'carbon_monoxide', 'friendly_name': 'openweathermap Carbon monoxide', 'state_class': , - 'unit_of_measurement': 'ppm', + 'unit_of_measurement': 'µg/m³', }), 'context': , 'entity_id': 'sensor.openweathermap_carbon_monoxide', From 9997fc11b186e8f406d8d314037bf80ae82f7e73 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 10 Jun 2025 14:31:18 +0200 Subject: [PATCH 140/266] Handle changes to source entity in derivative helper (#146407) * Handle changes to source entity in derivative helper * Rename helper function, improve docstring * Add tests * Improve derivative tests * Deduplicate tests * Rename helpers/helper_entity.py to helpers/helper_integration.py * Rename tests --- .../components/derivative/__init__.py | 27 ++ .../components/switch_as_x/__init__.py | 79 +--- homeassistant/helpers/helper_integration.py | 105 +++++ tests/components/derivative/test_init.py | 279 +++++++++++- tests/helpers/test_helper_integration.py | 424 ++++++++++++++++++ 5 files changed, 847 insertions(+), 67 deletions(-) create mode 100644 homeassistant/helpers/helper_integration.py create mode 100644 tests/helpers/test_helper_integration.py diff --git a/homeassistant/components/derivative/__init__.py b/homeassistant/components/derivative/__init__.py index 5117663f3c5..5eb499b0efd 100644 --- a/homeassistant/components/derivative/__init__.py +++ b/homeassistant/components/derivative/__init__.py @@ -2,12 +2,18 @@ from __future__ import annotations +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_SOURCE, Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.device import ( + async_entity_id_to_device_id, async_remove_stale_devices_links_keep_entity_device, ) +from homeassistant.helpers.helper_integration import async_handle_source_entity_changes + +from .const import DOMAIN async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -17,6 +23,27 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, entry.entry_id, entry.options[CONF_SOURCE] ) + def set_source_entity_id_or_uuid(source_entity_id: str) -> None: + hass.config_entries.async_update_entry( + entry, + options={**entry.options, CONF_SOURCE: source_entity_id}, + ) + + entity_registry = er.async_get(hass) + entry.async_on_unload( + async_handle_source_entity_changes( + hass, + helper_config_entry_id=entry.entry_id, + get_helper_entity_id=lambda: entity_registry.async_get_entity_id( + SENSOR_DOMAIN, DOMAIN, entry.entry_id + ), + set_source_entity_id_or_uuid=set_source_entity_id_or_uuid, + source_device_id=async_entity_id_to_device_id( + hass, entry.options[CONF_SOURCE] + ), + source_entity_id_or_uuid=entry.options[CONF_SOURCE], + ) + ) await hass.config_entries.async_forward_entry_setups(entry, (Platform.SENSOR,)) entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) return True diff --git a/homeassistant/components/switch_as_x/__init__.py b/homeassistant/components/switch_as_x/__init__.py index 9e40a99299a..6e9e3a93b45 100644 --- a/homeassistant/components/switch_as_x/__init__.py +++ b/homeassistant/components/switch_as_x/__init__.py @@ -9,9 +9,9 @@ import voluptuous as vol from homeassistant.components.homeassistant import exposed_entities from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ENTITY_ID -from homeassistant.core import Event, HomeAssistant, callback, valid_entity_id +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.helpers.event import async_track_entity_registry_updated_event +from homeassistant.helpers.helper_integration import async_handle_source_entity_changes from .const import CONF_INVERT, CONF_TARGET_DOMAIN, DOMAIN from .light import LightSwitch @@ -45,7 +45,6 @@ def async_add_to_device( async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" entity_registry = er.async_get(hass) - device_registry = dr.async_get(hass) try: entity_id = er.async_validate_entity_id( entity_registry, entry.options[CONF_ENTITY_ID] @@ -58,72 +57,26 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) return False - async def async_registry_updated( - event: Event[er.EventEntityRegistryUpdatedData], - ) -> None: - """Handle entity registry update.""" - data = event.data - if data["action"] == "remove": - await hass.config_entries.async_remove(entry.entry_id) - - if data["action"] != "update": - return - - if "entity_id" in data["changes"]: - # Entity_id changed, update or reload the config entry - if valid_entity_id(entry.options[CONF_ENTITY_ID]): - # If the entity is pointed to by an entity ID, update the entry - hass.config_entries.async_update_entry( - entry, - options={**entry.options, CONF_ENTITY_ID: data["entity_id"]}, - ) - else: - await hass.config_entries.async_reload(entry.entry_id) - - if device_id and "device_id" in data["changes"]: - # Handle the wrapped switch being moved to a different device or removed - # from the device - if ( - not (entity_entry := entity_registry.async_get(data[CONF_ENTITY_ID])) - or not device_registry.async_get(device_id) - or entity_entry.device_id == device_id - ): - # No need to do any cleanup - return - - # The wrapped switch has been moved to a different device, update the - # switch_as_x entity and the device entry to include our config entry - switch_as_x_entity_id = entity_registry.async_get_entity_id( - entry.options[CONF_TARGET_DOMAIN], DOMAIN, entry.entry_id - ) - if switch_as_x_entity_id: - # Update the switch_as_x entity to point to the new device (or no device) - entity_registry.async_update_entity( - switch_as_x_entity_id, device_id=entity_entry.device_id - ) - - if entity_entry.device_id is not None: - device_registry.async_update_device( - entity_entry.device_id, add_config_entry_id=entry.entry_id - ) - - device_registry.async_update_device( - device_id, remove_config_entry_id=entry.entry_id - ) - - # Reload the config entry so the switch_as_x entity is recreated with - # correct device info - await hass.config_entries.async_reload(entry.entry_id) + def set_source_entity_id_or_uuid(source_entity_id: str) -> None: + hass.config_entries.async_update_entry( + entry, + options={**entry.options, CONF_ENTITY_ID: source_entity_id}, + ) entry.async_on_unload( - async_track_entity_registry_updated_event( - hass, entity_id, async_registry_updated + async_handle_source_entity_changes( + hass, + helper_config_entry_id=entry.entry_id, + get_helper_entity_id=lambda: entity_registry.async_get_entity_id( + entry.options[CONF_TARGET_DOMAIN], DOMAIN, entry.entry_id + ), + set_source_entity_id_or_uuid=set_source_entity_id_or_uuid, + source_device_id=async_add_to_device(hass, entry, entity_id), + source_entity_id_or_uuid=entry.options[CONF_ENTITY_ID], ) ) entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) - device_id = async_add_to_device(hass, entry, entity_id) - await hass.config_entries.async_forward_entry_setups( entry, (entry.options[CONF_TARGET_DOMAIN],) ) diff --git a/homeassistant/helpers/helper_integration.py b/homeassistant/helpers/helper_integration.py new file mode 100644 index 00000000000..4f39ef4c843 --- /dev/null +++ b/homeassistant/helpers/helper_integration.py @@ -0,0 +1,105 @@ +"""Helpers for helper integrations.""" + +from __future__ import annotations + +from collections.abc import Callable + +from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, valid_entity_id + +from . import device_registry as dr, entity_registry as er +from .event import async_track_entity_registry_updated_event + + +def async_handle_source_entity_changes( + hass: HomeAssistant, + *, + helper_config_entry_id: str, + get_helper_entity_id: Callable[[], str | None], + set_source_entity_id_or_uuid: Callable[[str], None], + source_device_id: str | None, + source_entity_id_or_uuid: str, +) -> CALLBACK_TYPE: + """Handle changes to a helper entity's source entity. + + The following changes are handled: + - Entity removal: If the source entity is removed, the helper config entry + is removed, and the helper entity is cleaned up. + - Entity ID changed: If the source entity's entity ID changes and the source + entity is identified by an entity ID, the set_source_entity_id_or_uuid is + called. If the source entity is identified by a UUID, the helper config entry + is reloaded. + - Source entity moved to another device: The helper entity is updated to link + to the new device, and the helper config entry removed from the old device + and added to the new device. Then the helper config entry is reloaded. + - Source entity removed from the device: The helper entity is updated to link + to no device, and the helper config entry removed from the old device. Then + the helper config entry is reloaded. + """ + + async def async_registry_updated( + event: Event[er.EventEntityRegistryUpdatedData], + ) -> None: + """Handle entity registry update.""" + nonlocal source_device_id + + data = event.data + if data["action"] == "remove": + await hass.config_entries.async_remove(helper_config_entry_id) + + if data["action"] != "update": + return + + if "entity_id" in data["changes"]: + # Entity_id changed, update or reload the config entry + if valid_entity_id(source_entity_id_or_uuid): + # If the entity is pointed to by an entity ID, update the entry + set_source_entity_id_or_uuid(data["entity_id"]) + else: + await hass.config_entries.async_reload(helper_config_entry_id) + + if not source_device_id or "device_id" not in data["changes"]: + return + + # Handle the source entity being moved to a different device or removed + # from the device + if ( + not (source_entity_entry := entity_registry.async_get(data["entity_id"])) + or not device_registry.async_get(source_device_id) + or source_entity_entry.device_id == source_device_id + ): + # No need to do any cleanup + return + + # The source entity has been moved to a different device, update the helper + # helper entity to link to the new device and the helper device to include + # the helper config entry + helper_entity_id = get_helper_entity_id() + if helper_entity_id: + # Update the helper entity to link to the new device (or no device) + entity_registry.async_update_entity( + helper_entity_id, device_id=source_entity_entry.device_id + ) + + if source_entity_entry.device_id is not None: + device_registry.async_update_device( + source_entity_entry.device_id, + add_config_entry_id=helper_config_entry_id, + ) + + device_registry.async_update_device( + source_device_id, remove_config_entry_id=helper_config_entry_id + ) + source_device_id = source_entity_entry.device_id + + # Reload the config entry so the helper entity is recreated with + # correct device info + await hass.config_entries.async_reload(helper_config_entry_id) + + device_registry = dr.async_get(hass) + entity_registry = er.async_get(hass) + source_entity_id = er.async_validate_entity_id( + entity_registry, source_entity_id_or_uuid + ) + return async_track_entity_registry_updated_event( + hass, source_entity_id, async_registry_updated + ) diff --git a/tests/components/derivative/test_init.py b/tests/components/derivative/test_init.py index 32802080e39..f75d5940da7 100644 --- a/tests/components/derivative/test_init.py +++ b/tests/components/derivative/test_init.py @@ -1,23 +1,103 @@ """Test the Derivative integration.""" +from unittest.mock import patch + import pytest +from homeassistant.components import derivative +from homeassistant.components.derivative.config_flow import ConfigFlowHandler from homeassistant.components.derivative.const import DOMAIN -from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import Event, HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.event import async_track_entity_registry_updated_event from tests.common import MockConfigEntry -@pytest.mark.parametrize("platform", ["sensor"]) +@pytest.fixture +def sensor_config_entry(hass: HomeAssistant) -> er.RegistryEntry: + """Fixture to create a sensor config entry.""" + sensor_config_entry = MockConfigEntry() + sensor_config_entry.add_to_hass(hass) + return sensor_config_entry + + +@pytest.fixture +def sensor_device( + device_registry: dr.DeviceRegistry, sensor_config_entry: ConfigEntry +) -> dr.DeviceEntry: + """Fixture to create a sensor device.""" + return device_registry.async_get_or_create( + config_entry_id=sensor_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + + +@pytest.fixture +def sensor_entity_entry( + entity_registry: er.EntityRegistry, + sensor_config_entry: ConfigEntry, + sensor_device: dr.DeviceEntry, +) -> er.RegistryEntry: + """Fixture to create a sensor entity entry.""" + return entity_registry.async_get_or_create( + "sensor", + "test", + "unique", + config_entry=sensor_config_entry, + device_id=sensor_device.id, + original_name="ABC", + ) + + +@pytest.fixture +def derivative_config_entry( + hass: HomeAssistant, + sensor_entity_entry: er.RegistryEntry, +) -> MockConfigEntry: + """Fixture to create a derivative config entry.""" + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "name": "My derivative", + "round": 1.0, + "source": sensor_entity_entry.entity_id, + "time_window": {"seconds": 0.0}, + "unit_prefix": "k", + "unit_time": "min", + }, + title="My derivative", + version=ConfigFlowHandler.VERSION, + minor_version=ConfigFlowHandler.MINOR_VERSION, + ) + + config_entry.add_to_hass(hass) + + return config_entry + + +def track_entity_registry_actions(hass: HomeAssistant, entity_id: str) -> list[str]: + """Track entity registry actions for an entity.""" + events = [] + + def add_event(event: Event[er.EventEntityRegistryUpdatedData]) -> None: + """Add entity registry updated event to the list.""" + events.append(event.data["action"]) + + async_track_entity_registry_updated_event(hass, entity_id, add_event) + + return events + + async def test_setup_and_remove_config_entry( hass: HomeAssistant, entity_registry: er.EntityRegistry, - platform: str, ) -> None: """Test setting up and removing a config entry.""" input_sensor_entity_id = "sensor.input" - derivative_entity_id = f"{platform}.my_derivative" + derivative_entity_id = "sensor.my_derivative" # Setup the config entry config_entry = MockConfigEntry( @@ -147,3 +227,194 @@ async def test_device_cleaning( derivative_config_entry.entry_id ) assert len(devices_after_reload) == 1 + + +async def test_async_handle_source_entity_changes_source_entity_removed( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + derivative_config_entry: MockConfigEntry, + sensor_config_entry: ConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, +) -> None: + """Test the derivative config entry is removed when the source entity is removed.""" + # Add another config entry to the sensor device + other_config_entry = MockConfigEntry() + other_config_entry.add_to_hass(hass) + device_registry.async_update_device( + sensor_device.id, add_config_entry_id=other_config_entry.entry_id + ) + + assert await hass.config_entries.async_setup(derivative_config_entry.entry_id) + await hass.async_block_till_done() + + derivative_entity_entry = entity_registry.async_get("sensor.my_derivative") + assert derivative_entity_entry.device_id == sensor_entity_entry.device_id + + sensor_device = device_registry.async_get(sensor_device.id) + assert derivative_config_entry.entry_id in sensor_device.config_entries + + events = track_entity_registry_actions(hass, derivative_entity_entry.entity_id) + + # Remove the source sensor's config entry from the device, this removes the + # source sensor + with patch( + "homeassistant.components.derivative.async_unload_entry", + wraps=derivative.async_unload_entry, + ) as mock_unload_entry: + device_registry.async_update_device( + sensor_device.id, remove_config_entry_id=sensor_config_entry.entry_id + ) + await hass.async_block_till_done() + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the derivative config entry is removed from the device + sensor_device = device_registry.async_get(sensor_device.id) + assert derivative_config_entry.entry_id not in sensor_device.config_entries + + # Check that the derivative config entry is removed + assert derivative_config_entry.entry_id not in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == ["remove"] + + +async def test_async_handle_source_entity_changes_source_entity_removed_from_device( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + derivative_config_entry: MockConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, +) -> None: + """Test the source entity removed from the source device.""" + assert await hass.config_entries.async_setup(derivative_config_entry.entry_id) + await hass.async_block_till_done() + + derivative_entity_entry = entity_registry.async_get("sensor.my_derivative") + assert derivative_entity_entry.device_id == sensor_entity_entry.device_id + + sensor_device = device_registry.async_get(sensor_device.id) + assert derivative_config_entry.entry_id in sensor_device.config_entries + + events = track_entity_registry_actions(hass, derivative_entity_entry.entity_id) + + # Remove the source sensor from the device + with patch( + "homeassistant.components.derivative.async_unload_entry", + wraps=derivative.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + sensor_entity_entry.entity_id, device_id=None + ) + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the derivative config entry is removed from the device + sensor_device = device_registry.async_get(sensor_device.id) + assert derivative_config_entry.entry_id not in sensor_device.config_entries + + # Check that the derivative config entry is not removed + assert derivative_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == ["update"] + + +async def test_async_handle_source_entity_changes_source_entity_moved_other_device( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + derivative_config_entry: MockConfigEntry, + sensor_config_entry: ConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, +) -> None: + """Test the source entity is moved to another device.""" + sensor_device_2 = device_registry.async_get_or_create( + config_entry_id=sensor_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:FF")}, + ) + + assert await hass.config_entries.async_setup(derivative_config_entry.entry_id) + await hass.async_block_till_done() + + derivative_entity_entry = entity_registry.async_get("sensor.my_derivative") + assert derivative_entity_entry.device_id == sensor_entity_entry.device_id + + sensor_device = device_registry.async_get(sensor_device.id) + assert derivative_config_entry.entry_id in sensor_device.config_entries + sensor_device_2 = device_registry.async_get(sensor_device_2.id) + assert derivative_config_entry.entry_id not in sensor_device_2.config_entries + + events = track_entity_registry_actions(hass, derivative_entity_entry.entity_id) + + # Move the source sensor to another device + with patch( + "homeassistant.components.derivative.async_unload_entry", + wraps=derivative.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + sensor_entity_entry.entity_id, device_id=sensor_device_2.id + ) + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the derivative config entry is moved to the other device + sensor_device = device_registry.async_get(sensor_device.id) + assert derivative_config_entry.entry_id not in sensor_device.config_entries + sensor_device_2 = device_registry.async_get(sensor_device_2.id) + assert derivative_config_entry.entry_id in sensor_device_2.config_entries + + # Check that the derivative config entry is not removed + assert derivative_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == ["update"] + + +async def test_async_handle_source_entity_new_entity_id( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + derivative_config_entry: MockConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, +) -> None: + """Test the source entity's entity ID is changed.""" + assert await hass.config_entries.async_setup(derivative_config_entry.entry_id) + await hass.async_block_till_done() + + derivative_entity_entry = entity_registry.async_get("sensor.my_derivative") + assert derivative_entity_entry.device_id == sensor_entity_entry.device_id + + sensor_device = device_registry.async_get(sensor_device.id) + assert derivative_config_entry.entry_id in sensor_device.config_entries + + events = track_entity_registry_actions(hass, derivative_entity_entry.entity_id) + + # Change the source entity's entity ID + with patch( + "homeassistant.components.derivative.async_unload_entry", + wraps=derivative.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + sensor_entity_entry.entity_id, new_entity_id="sensor.new_entity_id" + ) + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the derivative config entry is updated with the new entity ID + assert derivative_config_entry.options["source"] == "sensor.new_entity_id" + + # Check that the helper config is still in the device + sensor_device = device_registry.async_get(sensor_device.id) + assert derivative_config_entry.entry_id in sensor_device.config_entries + + # Check that the derivative config entry is not removed + assert derivative_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == [] diff --git a/tests/helpers/test_helper_integration.py b/tests/helpers/test_helper_integration.py new file mode 100644 index 00000000000..25d490c27bb --- /dev/null +++ b/tests/helpers/test_helper_integration.py @@ -0,0 +1,424 @@ +"""Tests for the helper entity helpers.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, Mock + +import pytest + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import Event, HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.event import async_track_entity_registry_updated_event +from homeassistant.helpers.helper_integration import async_handle_source_entity_changes + +from tests.common import ( + MockConfigEntry, + MockModule, + mock_config_flow, + mock_integration, + mock_platform, +) + +HELPER_DOMAIN = "helper" +SOURCE_DOMAIN = "test" + + +@pytest.fixture +def source_config_entry(hass: HomeAssistant) -> er.RegistryEntry: + """Fixture to create a source config entry.""" + source_config_entry = MockConfigEntry() + source_config_entry.add_to_hass(hass) + return source_config_entry + + +@pytest.fixture +def source_device( + device_registry: dr.DeviceRegistry, + source_config_entry: ConfigEntry, +) -> dr.DeviceEntry: + """Fixture to create a source device.""" + return device_registry.async_get_or_create( + config_entry_id=source_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + + +@pytest.fixture +def source_entity_entry( + entity_registry: er.EntityRegistry, + source_config_entry: ConfigEntry, + source_device: dr.DeviceEntry, +) -> er.RegistryEntry: + """Fixture to create a source entity entry.""" + return entity_registry.async_get_or_create( + "sensor", + SOURCE_DOMAIN, + "unique", + config_entry=source_config_entry, + device_id=source_device.id, + original_name="ABC", + ) + + +@pytest.fixture +def helper_config_entry( + hass: HomeAssistant, + source_entity_entry: er.RegistryEntry, + use_entity_registry_id: bool, +) -> MockConfigEntry: + """Fixture to create a helper config entry.""" + config_entry = MockConfigEntry( + data={}, + domain=HELPER_DOMAIN, + options={ + "name": "My helper", + "round": 1.0, + "source": source_entity_entry.id + if use_entity_registry_id + else source_entity_entry.entity_id, + "time_window": {"seconds": 0.0}, + "unit_prefix": "k", + "unit_time": "min", + }, + title="My helper", + ) + + config_entry.add_to_hass(hass) + + return config_entry + + +@pytest.fixture +def mock_helper_flow() -> Generator[None]: + """Mock helper config flow.""" + + class MockConfigFlow: + """Mock the helper config flow.""" + + VERSION = 1 + MINOR_VERSION = 1 + + with mock_config_flow(HELPER_DOMAIN, MockConfigFlow): + yield + + +@pytest.fixture +def helper_entity_entry( + entity_registry: er.EntityRegistry, + helper_config_entry: ConfigEntry, + source_device: dr.DeviceEntry, +) -> er.RegistryEntry: + """Fixture to create a helper entity entry.""" + return entity_registry.async_get_or_create( + "sensor", + HELPER_DOMAIN, + helper_config_entry.entry_id, + config_entry=helper_config_entry, + device_id=source_device.id, + original_name="ABC", + ) + + +@pytest.fixture +def async_remove_entry() -> AsyncMock: + """Fixture to mock async_remove_entry.""" + return AsyncMock(return_value=True) + + +@pytest.fixture +def async_unload_entry() -> AsyncMock: + """Fixture to mock async_unload_entry.""" + return AsyncMock(return_value=True) + + +@pytest.fixture +def set_source_entity_id_or_uuid() -> AsyncMock: + """Fixture to mock async_unload_entry.""" + return Mock() + + +@pytest.fixture +def mock_helper_integration( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + helper_config_entry: MockConfigEntry, + source_entity_entry: er.RegistryEntry, + async_remove_entry: AsyncMock, + async_unload_entry: AsyncMock, + set_source_entity_id_or_uuid: Mock, +) -> None: + """Mock the helper integration.""" + + def get_helper_entity_id() -> str | None: + """Get the helper entity ID.""" + return entity_registry.async_get_entity_id( + "sensor", HELPER_DOMAIN, helper_config_entry.entry_id + ) + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Mock setup entry.""" + async_handle_source_entity_changes( + hass, + helper_config_entry_id=helper_config_entry.entry_id, + get_helper_entity_id=get_helper_entity_id, + set_source_entity_id_or_uuid=set_source_entity_id_or_uuid, + source_device_id=source_entity_entry.device_id, + source_entity_id_or_uuid=helper_config_entry.options["source"], + ) + return True + + mock_integration( + hass, + MockModule( + HELPER_DOMAIN, + async_remove_entry=async_remove_entry, + async_setup_entry=async_setup_entry, + async_unload_entry=async_unload_entry, + ), + ) + mock_platform(hass, f"{HELPER_DOMAIN}.config_flow", None) + + +def track_entity_registry_actions(hass: HomeAssistant, entity_id: str) -> list[str]: + """Track entity registry actions for an entity.""" + events = [] + + def add_event(event: Event[er.EventEntityRegistryUpdatedData]) -> None: + """Add entity registry updated event to the list.""" + events.append(event.data["action"]) + + async_track_entity_registry_updated_event(hass, entity_id, add_event) + + return events + + +@pytest.mark.parametrize("use_entity_registry_id", [True, False]) +@pytest.mark.usefixtures("mock_helper_flow", "mock_helper_integration") +async def test_async_handle_source_entity_changes_source_entity_removed( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + helper_config_entry: MockConfigEntry, + helper_entity_entry: er.RegistryEntry, + source_config_entry: ConfigEntry, + source_device: dr.DeviceEntry, + source_entity_entry: er.RegistryEntry, + async_remove_entry: AsyncMock, + async_unload_entry: AsyncMock, + set_source_entity_id_or_uuid: Mock, +) -> None: + """Test the helper config entry is removed when the source entity is removed.""" + # Add the helper config entry to the source device + device_registry.async_update_device( + source_device.id, add_config_entry_id=helper_config_entry.entry_id + ) + # Add another config entry to the source device + other_config_entry = MockConfigEntry() + other_config_entry.add_to_hass(hass) + device_registry.async_update_device( + source_device.id, add_config_entry_id=other_config_entry.entry_id + ) + + assert await hass.config_entries.async_setup(helper_config_entry.entry_id) + await hass.async_block_till_done() + + # Check preconditions + helper_entity_entry = entity_registry.async_get(helper_entity_entry.entity_id) + assert helper_entity_entry.device_id == source_entity_entry.device_id + source_device = device_registry.async_get(source_device.id) + assert helper_config_entry.entry_id in source_device.config_entries + + events = track_entity_registry_actions(hass, helper_entity_entry.entity_id) + + # Remove the source entitys's config entry from the device, this removes the + # source entity + device_registry.async_update_device( + source_device.id, remove_config_entry_id=source_config_entry.entry_id + ) + await hass.async_block_till_done() + await hass.async_block_till_done() + + # Check that the helper config entry is unloaded and removed + async_unload_entry.assert_called_once() + async_remove_entry.assert_called_once() + set_source_entity_id_or_uuid.assert_not_called() + + # Check that the helper config entry is removed from the device + source_device = device_registry.async_get(source_device.id) + assert helper_config_entry.entry_id not in source_device.config_entries + + # Check that the helper config entry is removed + assert helper_config_entry.entry_id not in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == ["remove"] + + +@pytest.mark.parametrize("use_entity_registry_id", [True, False]) +@pytest.mark.usefixtures("mock_helper_flow", "mock_helper_integration") +async def test_async_handle_source_entity_changes_source_entity_removed_from_device( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + helper_config_entry: MockConfigEntry, + helper_entity_entry: er.RegistryEntry, + source_device: dr.DeviceEntry, + source_entity_entry: er.RegistryEntry, + async_remove_entry: AsyncMock, + async_unload_entry: AsyncMock, + set_source_entity_id_or_uuid: Mock, +) -> None: + """Test the source entity removed from the source device.""" + # Add the helper config entry to the source device + device_registry.async_update_device( + source_device.id, add_config_entry_id=helper_config_entry.entry_id + ) + + assert await hass.config_entries.async_setup(helper_config_entry.entry_id) + await hass.async_block_till_done() + + # Check preconditions + helper_entity_entry = entity_registry.async_get(helper_entity_entry.entity_id) + assert helper_entity_entry.device_id == source_entity_entry.device_id + + source_device = device_registry.async_get(source_device.id) + assert helper_config_entry.entry_id in source_device.config_entries + + events = track_entity_registry_actions(hass, helper_entity_entry.entity_id) + + # Remove the source entity from the device + entity_registry.async_update_entity(source_entity_entry.entity_id, device_id=None) + await hass.async_block_till_done() + async_remove_entry.assert_not_called() + async_unload_entry.assert_called_once() + set_source_entity_id_or_uuid.assert_not_called() + + # Check that the helper config entry is removed from the device + source_device = device_registry.async_get(source_device.id) + assert helper_config_entry.entry_id not in source_device.config_entries + + # Check that the helper config entry is not removed + assert helper_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == ["update"] + + +@pytest.mark.parametrize("use_entity_registry_id", [True, False]) +@pytest.mark.usefixtures("mock_helper_flow", "mock_helper_integration") +async def test_async_handle_source_entity_changes_source_entity_moved_other_device( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + helper_config_entry: MockConfigEntry, + helper_entity_entry: er.RegistryEntry, + source_config_entry: ConfigEntry, + source_device: dr.DeviceEntry, + source_entity_entry: er.RegistryEntry, + async_remove_entry: AsyncMock, + async_unload_entry: AsyncMock, + set_source_entity_id_or_uuid: Mock, +) -> None: + """Test the source entity is moved to another device.""" + # Add the helper config entry to the source device + device_registry.async_update_device( + source_device.id, add_config_entry_id=helper_config_entry.entry_id + ) + + # Create another device to move the source entity to + source_device_2 = device_registry.async_get_or_create( + config_entry_id=source_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:FF")}, + ) + + assert await hass.config_entries.async_setup(helper_config_entry.entry_id) + await hass.async_block_till_done() + + # Check preconditions + helper_entity_entry = entity_registry.async_get(helper_entity_entry.entity_id) + assert helper_entity_entry.device_id == source_entity_entry.device_id + + source_device = device_registry.async_get(source_device.id) + assert helper_config_entry.entry_id in source_device.config_entries + source_device_2 = device_registry.async_get(source_device_2.id) + assert helper_config_entry.entry_id not in source_device_2.config_entries + + events = track_entity_registry_actions(hass, helper_entity_entry.entity_id) + + # Move the source entity to another device + entity_registry.async_update_entity( + source_entity_entry.entity_id, device_id=source_device_2.id + ) + await hass.async_block_till_done() + async_remove_entry.assert_not_called() + async_unload_entry.assert_called_once() + set_source_entity_id_or_uuid.assert_not_called() + + # Check that the helper config entry is moved to the other device + source_device = device_registry.async_get(source_device.id) + assert helper_config_entry.entry_id not in source_device.config_entries + source_device_2 = device_registry.async_get(source_device_2.id) + assert helper_config_entry.entry_id in source_device_2.config_entries + + # Check that the helper config entry is not removed + assert helper_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == ["update"] + + +@pytest.mark.parametrize( + ("use_entity_registry_id", "unload_calls", "set_source_entity_id_calls"), + [(True, 1, 0), (False, 0, 1)], +) +@pytest.mark.usefixtures("mock_helper_flow", "mock_helper_integration") +async def test_async_handle_source_entity_new_entity_id( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + helper_config_entry: MockConfigEntry, + helper_entity_entry: er.RegistryEntry, + source_device: dr.DeviceEntry, + source_entity_entry: er.RegistryEntry, + async_remove_entry: AsyncMock, + async_unload_entry: AsyncMock, + set_source_entity_id_or_uuid: Mock, + unload_calls: int, + set_source_entity_id_calls: int, +) -> None: + """Test the source entity's entity ID is changed.""" + # Add the helper config entry to the source device + device_registry.async_update_device( + source_device.id, add_config_entry_id=helper_config_entry.entry_id + ) + + assert await hass.config_entries.async_setup(helper_config_entry.entry_id) + await hass.async_block_till_done() + + # Check preconditions + helper_entity_entry = entity_registry.async_get(helper_entity_entry.entity_id) + assert helper_entity_entry.device_id == source_entity_entry.device_id + + source_device = device_registry.async_get(source_device.id) + assert helper_config_entry.entry_id in source_device.config_entries + + events = track_entity_registry_actions(hass, helper_entity_entry.entity_id) + + # Change the source entity's entity ID + entity_registry.async_update_entity( + source_entity_entry.entity_id, new_entity_id="sensor.new_entity_id" + ) + await hass.async_block_till_done() + async_remove_entry.assert_not_called() + assert len(async_unload_entry.mock_calls) == unload_calls + assert len(set_source_entity_id_or_uuid.mock_calls) == set_source_entity_id_calls + + # Check that the helper config is still in the device + source_device = device_registry.async_get(source_device.id) + assert helper_config_entry.entry_id in source_device.config_entries + + # Check that the helper config entry is not removed + assert helper_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == [] From ec30b12fd1e668049299eeba47caa0caa9ebc79e Mon Sep 17 00:00:00 2001 From: Whitney Young Date: Tue, 10 Jun 2025 08:35:40 -0700 Subject: [PATCH 141/266] Fix initial state of UV protection window (#146408) The `binary_sensor` is created when the config entry is loaded after the `async_config_entry_first_refresh` has completed (during the forward of setup to platforms). Therefore, the update coordinator will already have data and will not trigger the invocation of `_handle_coordinator_update`. Fixing this just means performing the same update at initialization. --- homeassistant/components/openuv/binary_sensor.py | 6 ++++-- homeassistant/components/openuv/entity.py | 5 +++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/openuv/binary_sensor.py b/homeassistant/components/openuv/binary_sensor.py index f45404ce38e..09c9ab75192 100644 --- a/homeassistant/components/openuv/binary_sensor.py +++ b/homeassistant/components/openuv/binary_sensor.py @@ -49,6 +49,10 @@ class OpenUvBinarySensor(OpenUvEntity, BinarySensorEntity): @callback def _handle_coordinator_update(self) -> None: """Update the entity from the latest data.""" + self._update_attrs() + super()._handle_coordinator_update() + + def _update_attrs(self) -> None: data = self.coordinator.data for key in ("from_time", "to_time", "from_uv", "to_uv"): @@ -78,5 +82,3 @@ class OpenUvBinarySensor(OpenUvEntity, BinarySensorEntity): ATTR_PROTECTION_WINDOW_STARTING_TIME: as_local(from_dt), } ) - - super()._handle_coordinator_update() diff --git a/homeassistant/components/openuv/entity.py b/homeassistant/components/openuv/entity.py index f3015815bf1..2303f21f2b8 100644 --- a/homeassistant/components/openuv/entity.py +++ b/homeassistant/components/openuv/entity.py @@ -31,3 +31,8 @@ class OpenUvEntity(CoordinatorEntity): name="OpenUV", entry_type=DeviceEntryType.SERVICE, ) + + self._update_attrs() + + def _update_attrs(self) -> None: + """Override point for updating attributes during init.""" From 97d91ddddba41ed0457aa4df6e30287924aac07d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 10 Jun 2025 04:44:34 -0500 Subject: [PATCH 142/266] Bump propcache to 0.3.2 (#146418) --- 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 a994e1d6333..b34d994b8a5 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -50,7 +50,7 @@ orjson==3.10.18 packaging>=23.1 paho-mqtt==2.1.0 Pillow==11.2.1 -propcache==0.3.1 +propcache==0.3.2 psutil-home-assistant==0.0.1 PyJWT==2.10.1 pymicro-vad==1.0.1 diff --git a/pyproject.toml b/pyproject.toml index 7f458bebf39..65157f8452a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -84,7 +84,7 @@ dependencies = [ # PyJWT has loose dependency. We want the latest one. "cryptography==45.0.1", "Pillow==11.2.1", - "propcache==0.3.1", + "propcache==0.3.2", "pyOpenSSL==25.1.0", "orjson==3.10.18", "packaging>=23.1", diff --git a/requirements.txt b/requirements.txt index 88576cd0c4c..2ded4aeca71 100644 --- a/requirements.txt +++ b/requirements.txt @@ -36,7 +36,7 @@ numpy==2.2.2 PyJWT==2.10.1 cryptography==45.0.1 Pillow==11.2.1 -propcache==0.3.1 +propcache==0.3.2 pyOpenSSL==25.1.0 orjson==3.10.18 packaging>=23.1 From 2b08c4c344583996ad6905eafcdf790a9712c893 Mon Sep 17 00:00:00 2001 From: Jamin Date: Tue, 10 Jun 2025 09:22:53 -0500 Subject: [PATCH 143/266] Check hangup error in voip (#146423) Check hangup error Prevent an error where the call end future may have already been set when a hangup is detected. --- homeassistant/components/voip/assist_satellite.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/voip/assist_satellite.py b/homeassistant/components/voip/assist_satellite.py index 7b34d7a11ba..ac8065cabf7 100644 --- a/homeassistant/components/voip/assist_satellite.py +++ b/homeassistant/components/voip/assist_satellite.py @@ -336,7 +336,8 @@ class VoipAssistSatellite(VoIPEntity, AssistSatelliteEntity, RtpDatagramProtocol if self._run_pipeline_task is not None: _LOGGER.debug("Cancelling running pipeline") self._run_pipeline_task.cancel() - self._call_end_future.set_result(None) + if not self._call_end_future.done(): + self._call_end_future.set_result(None) self.disconnect() break From 41abc8404de78145b45a08e3bcbbd6712eee1182 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 10 Jun 2025 04:26:29 -0500 Subject: [PATCH 144/266] Bump yarl to 1.20.1 (#146424) --- 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 b34d994b8a5..ae8b93788b7 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -74,7 +74,7 @@ voluptuous-openapi==0.1.0 voluptuous-serialize==2.6.0 voluptuous==0.15.2 webrtc-models==0.3.0 -yarl==1.20.0 +yarl==1.20.1 zeroconf==0.147.0 # Constrain pycryptodome to avoid vulnerability diff --git a/pyproject.toml b/pyproject.toml index 65157f8452a..51f89f932bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -121,7 +121,7 @@ dependencies = [ "voluptuous==0.15.2", "voluptuous-serialize==2.6.0", "voluptuous-openapi==0.1.0", - "yarl==1.20.0", + "yarl==1.20.1", "webrtc-models==0.3.0", "zeroconf==0.147.0", ] diff --git a/requirements.txt b/requirements.txt index 2ded4aeca71..0c5927dd9e5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -58,6 +58,6 @@ uv==0.7.1 voluptuous==0.15.2 voluptuous-serialize==2.6.0 voluptuous-openapi==0.1.0 -yarl==1.20.0 +yarl==1.20.1 webrtc-models==0.3.0 zeroconf==0.147.0 From 4f0e4bc1ca89516e54a433364813ed08af076171 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 10 Jun 2025 02:39:53 -0500 Subject: [PATCH 145/266] Bump aiohttp to 3.12.12 (#146426) --- 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 ae8b93788b7..f62c1c899ba 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -6,7 +6,7 @@ aiodns==3.4.0 aiohasupervisor==0.3.1 aiohttp-asyncmdnsresolver==0.1.1 aiohttp-fast-zlib==0.3.0 -aiohttp==3.12.11 +aiohttp==3.12.12 aiohttp_cors==0.7.0 aiousbwatcher==1.1.1 aiozoneinfo==0.2.3 diff --git a/pyproject.toml b/pyproject.toml index 51f89f932bc..c31deb67dcf 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.11", + "aiohttp==3.12.12", "aiohttp_cors==0.7.0", "aiohttp-fast-zlib==0.3.0", "aiohttp-asyncmdnsresolver==0.1.1", diff --git a/requirements.txt b/requirements.txt index 0c5927dd9e5..c07d0e282a0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ # Home Assistant Core aiodns==3.4.0 aiohasupervisor==0.3.1 -aiohttp==3.12.11 +aiohttp==3.12.12 aiohttp_cors==0.7.0 aiohttp-fast-zlib==0.3.0 aiohttp-asyncmdnsresolver==0.1.1 From f945defa2b4e601e389cdba1a0b0d44e45dfd467 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 10 Jun 2025 11:14:31 +0200 Subject: [PATCH 146/266] Reformat Dockerfile to reduce merge conflicts (#146435) --- script/hassfest/docker.py | 7 +++++-- script/hassfest/docker/Dockerfile | 14 ++++++++++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/script/hassfest/docker.py b/script/hassfest/docker.py index 4bf6c3bb0a6..1f112c11b94 100644 --- a/script/hassfest/docker.py +++ b/script/hassfest/docker.py @@ -103,7 +103,10 @@ RUN --mount=from=ghcr.io/astral-sh/uv:{uv},source=/uv,target=/bin/uv \ --no-cache \ -c /usr/src/homeassistant/homeassistant/package_constraints.txt \ -r /usr/src/homeassistant/requirements.txt \ - stdlib-list==0.10.0 pipdeptree=={pipdeptree} tqdm=={tqdm} ruff=={ruff} \ + stdlib-list==0.10.0 \ + pipdeptree=={pipdeptree} \ + tqdm=={tqdm} \ + ruff=={ruff} \ {required_components_packages} LABEL "name"="hassfest" @@ -169,7 +172,7 @@ def _generate_hassfest_dockerimage( return File( _HASSFEST_TEMPLATE.format( timeout=timeout, - required_components_packages=" ".join(sorted(packages)), + required_components_packages=" \\\n ".join(sorted(packages)), **package_versions, ), config.root / "script/hassfest/docker/Dockerfile", diff --git a/script/hassfest/docker/Dockerfile b/script/hassfest/docker/Dockerfile index 981dc3344c0..830bdc4445e 100644 --- a/script/hassfest/docker/Dockerfile +++ b/script/hassfest/docker/Dockerfile @@ -24,8 +24,18 @@ RUN --mount=from=ghcr.io/astral-sh/uv:0.7.1,source=/uv,target=/bin/uv \ --no-cache \ -c /usr/src/homeassistant/homeassistant/package_constraints.txt \ -r /usr/src/homeassistant/requirements.txt \ - stdlib-list==0.10.0 pipdeptree==2.26.1 tqdm==4.67.1 ruff==0.11.0 \ - PyTurboJPEG==1.7.5 go2rtc-client==0.2.1 ha-ffmpeg==3.2.2 hassil==2.2.3 home-assistant-intents==2025.5.28 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2 + stdlib-list==0.10.0 \ + pipdeptree==2.26.1 \ + tqdm==4.67.1 \ + ruff==0.11.0 \ + PyTurboJPEG==1.7.5 \ + go2rtc-client==0.2.1 \ + ha-ffmpeg==3.2.2 \ + hassil==2.2.3 \ + home-assistant-intents==2025.5.28 \ + mutagen==1.47.0 \ + pymicro-vad==1.0.1 \ + pyspeex-noise==1.0.2 LABEL "name"="hassfest" LABEL "maintainer"="Home Assistant " From b222fe5afaa9f46e696b1ab708a293e66c1aaff7 Mon Sep 17 00:00:00 2001 From: tronikos Date: Tue, 10 Jun 2025 06:31:32 -0700 Subject: [PATCH 147/266] Handle grpc errors in Google Assistant SDK (#146438) --- .../google_assistant_sdk/helpers.py | 14 +++++++- .../google_assistant_sdk/strings.json | 5 +++ .../google_assistant_sdk/test_init.py | 27 ++++++++++++++- .../google_assistant_sdk/test_notify.py | 34 ++++++++++++++++--- 4 files changed, 74 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/google_assistant_sdk/helpers.py b/homeassistant/components/google_assistant_sdk/helpers.py index ca774bed77e..b319e1e432c 100644 --- a/homeassistant/components/google_assistant_sdk/helpers.py +++ b/homeassistant/components/google_assistant_sdk/helpers.py @@ -12,6 +12,7 @@ import aiohttp from aiohttp import web from gassist_text import TextAssistant from google.oauth2.credentials import Credentials +from grpc import RpcError from homeassistant.components.http import HomeAssistantView from homeassistant.components.media_player import ( @@ -25,6 +26,7 @@ from homeassistant.components.media_player import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ENTITY_ID, CONF_ACCESS_TOKEN from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session from homeassistant.helpers.event import async_call_later @@ -83,7 +85,17 @@ async def async_send_text_commands( ) as assistant: command_response_list = [] for command in commands: - resp = await hass.async_add_executor_job(assistant.assist, command) + try: + resp = await hass.async_add_executor_job(assistant.assist, command) + except RpcError as err: + _LOGGER.error( + "Failed to send command '%s' to Google Assistant: %s", + command, + err, + ) + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="grpc_error" + ) from err text_response = resp[0] _LOGGER.debug("command: %s\nresponse: %s", command, text_response) audio_response = resp[2] diff --git a/homeassistant/components/google_assistant_sdk/strings.json b/homeassistant/components/google_assistant_sdk/strings.json index 87c93023900..885ff0aad71 100644 --- a/homeassistant/components/google_assistant_sdk/strings.json +++ b/homeassistant/components/google_assistant_sdk/strings.json @@ -57,5 +57,10 @@ } } } + }, + "exceptions": { + "grpc_error": { + "message": "Failed to communicate with Google Assistant" + } } } diff --git a/tests/components/google_assistant_sdk/test_init.py b/tests/components/google_assistant_sdk/test_init.py index f986497ed29..9bb08c802c2 100644 --- a/tests/components/google_assistant_sdk/test_init.py +++ b/tests/components/google_assistant_sdk/test_init.py @@ -6,6 +6,7 @@ import time from unittest.mock import call, patch import aiohttp +from grpc import RpcError import pytest from homeassistant.components import conversation @@ -13,6 +14,7 @@ from homeassistant.components.google_assistant_sdk import DOMAIN from homeassistant.components.google_assistant_sdk.const import SUPPORTED_LANGUAGE_CODES from homeassistant.config_entries import ConfigEntryState from homeassistant.core import Context, HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow @@ -231,11 +233,34 @@ async def test_send_text_command_expired_token_refresh_failure( {"command": "turn on tv"}, blocking=True, ) - await hass.async_block_till_done() assert any(entry.async_get_active_flows(hass, {"reauth"})) == requires_reauth +async def test_send_text_command_grpc_error( + hass: HomeAssistant, + setup_integration: ComponentSetup, +) -> None: + """Test service call send_text_command when RpcError is raised.""" + await setup_integration() + + command = "turn on home assistant unsupported device" + with ( + patch( + "homeassistant.components.google_assistant_sdk.helpers.TextAssistant.assist", + side_effect=RpcError(), + ) as mock_assist_call, + pytest.raises(HomeAssistantError), + ): + await hass.services.async_call( + DOMAIN, + "send_text_command", + {"command": command}, + blocking=True, + ) + mock_assist_call.assert_called_once_with(command) + + async def test_send_text_command_media_player( hass: HomeAssistant, setup_integration: ComponentSetup, diff --git a/tests/components/google_assistant_sdk/test_notify.py b/tests/components/google_assistant_sdk/test_notify.py index 266846b17e1..ca4162c9e7a 100644 --- a/tests/components/google_assistant_sdk/test_notify.py +++ b/tests/components/google_assistant_sdk/test_notify.py @@ -2,6 +2,7 @@ from unittest.mock import call, patch +from grpc import RpcError import pytest from homeassistant.components import notify @@ -9,6 +10,7 @@ from homeassistant.components.google_assistant_sdk import DOMAIN from homeassistant.components.google_assistant_sdk.const import SUPPORTED_LANGUAGE_CODES from homeassistant.components.google_assistant_sdk.notify import broadcast_commands from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from .conftest import ComponentSetup, ExpectedCredentials @@ -45,8 +47,8 @@ async def test_broadcast_no_targets( notify.DOMAIN, DOMAIN, {notify.ATTR_MESSAGE: message}, + blocking=True, ) - await hass.async_block_till_done() mock_text_assistant.assert_called_once_with( ExpectedCredentials(), language_code, audio_out=False ) @@ -54,6 +56,30 @@ async def test_broadcast_no_targets( mock_text_assistant.assert_has_calls([call().__enter__().assist(expected_command)]) +async def test_broadcast_grpc_error( + hass: HomeAssistant, + setup_integration: ComponentSetup, +) -> None: + """Test broadcast handling when RpcError is raised.""" + await setup_integration() + + with ( + patch( + "homeassistant.components.google_assistant_sdk.helpers.TextAssistant.assist", + side_effect=RpcError(), + ) as mock_assist_call, + pytest.raises(HomeAssistantError), + ): + await hass.services.async_call( + notify.DOMAIN, + DOMAIN, + {notify.ATTR_MESSAGE: "Dinner is served"}, + blocking=True, + ) + + mock_assist_call.assert_called_once_with("broadcast Dinner is served") + + @pytest.mark.parametrize( ("language_code", "message", "target", "expected_command"), [ @@ -103,8 +129,8 @@ async def test_broadcast_one_target( notify.DOMAIN, DOMAIN, {notify.ATTR_MESSAGE: message, notify.ATTR_TARGET: [target]}, + blocking=True, ) - await hass.async_block_till_done() mock_assist_call.assert_called_once_with(expected_command) @@ -127,8 +153,8 @@ async def test_broadcast_two_targets( notify.DOMAIN, DOMAIN, {notify.ATTR_MESSAGE: message, notify.ATTR_TARGET: [target1, target2]}, + blocking=True, ) - await hass.async_block_till_done() mock_assist_call.assert_has_calls( [call(expected_command1), call(expected_command2)] ) @@ -148,8 +174,8 @@ async def test_broadcast_empty_message( notify.DOMAIN, DOMAIN, {notify.ATTR_MESSAGE: ""}, + blocking=True, ) - await hass.async_block_till_done() mock_assist_call.assert_not_called() From ba19d4f0431ead5b421d8c96c1714029fd900f28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Diego=20Rodr=C3=ADguez=20Royo?= Date: Tue, 10 Jun 2025 11:56:24 +0200 Subject: [PATCH 148/266] Fix typo at application credentials string at Home Connect integration (#146442) Fix typos --- homeassistant/components/home_connect/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/home_connect/strings.json b/homeassistant/components/home_connect/strings.json index 71a1f1918f6..7aadf6b0dde 100644 --- a/homeassistant/components/home_connect/strings.json +++ b/homeassistant/components/home_connect/strings.json @@ -1,6 +1,6 @@ { "application_credentials": { - "description": "Login to Home Connect requires a client ID and secret. To acquire them, please follow the following steps.\n\n1. Visit the [Home Connect Developer Program website]({developer_dashboard_url}) and signup for a development account.\n1. Enter the email of your login for the original Home Connect app under **Default Home Connect User Account for Testing** in the sign up process.\n1. Go to the [Applications]({applications_url}) page and select [Register Application]({register_application_url}) and set the fields to the following values: \n\t* **Application ID**: Home Assistant (or any other name that makes sense)\n\t* **OAuth Flow**: Authorization Code Grant Flow\n\t* **Redirect URI**: `{redirect_url}`\n\nIn the newly created application's details, you will find the **Client ID** and the **Client Secret**." + "description": "Login to Home Connect requires a client ID and secret. To acquire them, please follow the following steps.\n\n1. Visit the [Home Connect Developer Program website]({developer_dashboard_url}) and sign up for a development account.\n1. Enter the email of your login for the original Home Connect app under **Default Home Connect User Account for Testing** in the signup process.\n1. Go to the [Applications]({applications_url}) page and select [Register Application]({register_application_url}) and set the fields to the following values: \n\t* **Application ID**: Home Assistant (or any other name that makes sense)\n\t* **OAuth Flow**: Authorization Code Grant Flow\n\t* **Redirect URI**: {redirect_url}\n\nIn the newly created application's details, you will find the **Client ID** and the **Client Secret**." }, "common": { "confirmed": "Confirmed", From b2d25b1883a8ca65218430394f83fcde40fce248 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Diego=20Rodr=C3=ADguez=20Royo?= Date: Tue, 10 Jun 2025 14:11:07 +0200 Subject: [PATCH 149/266] Improvements for Home Connect application credentials string (#146443) --- homeassistant/components/home_connect/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/home_connect/strings.json b/homeassistant/components/home_connect/strings.json index 7aadf6b0dde..1445a8eae08 100644 --- a/homeassistant/components/home_connect/strings.json +++ b/homeassistant/components/home_connect/strings.json @@ -1,6 +1,6 @@ { "application_credentials": { - "description": "Login to Home Connect requires a client ID and secret. To acquire them, please follow the following steps.\n\n1. Visit the [Home Connect Developer Program website]({developer_dashboard_url}) and sign up for a development account.\n1. Enter the email of your login for the original Home Connect app under **Default Home Connect User Account for Testing** in the signup process.\n1. Go to the [Applications]({applications_url}) page and select [Register Application]({register_application_url}) and set the fields to the following values: \n\t* **Application ID**: Home Assistant (or any other name that makes sense)\n\t* **OAuth Flow**: Authorization Code Grant Flow\n\t* **Redirect URI**: {redirect_url}\n\nIn the newly created application's details, you will find the **Client ID** and the **Client Secret**." + "description": "Login to Home Connect requires a client ID and secret. To acquire them, please follow the following steps.\n\n1. Visit the [Home Connect Developer Program website]({developer_dashboard_url}) and sign up for a development account.\n1. Enter the email of your login for the original Home Connect app under **Default Home Connect User Account for Testing** in the signup process.\n1. Go to the [Applications]({applications_url}) page and select [Register Application]({register_application_url}) and set the fields to the following values:\n * **Application ID**: Home Assistant (or any other name that makes sense)\n * **OAuth Flow**: Authorization Code Grant Flow\n * **Redirect URI**: `{redirect_url}`\n\nIn the newly created application's details, you will find the **Client ID** and the **Client Secret**." }, "common": { "confirmed": "Confirmed", From 6f4029983acdbbcdae1b4fdd937f9b788f3c4fc6 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 10 Jun 2025 15:00:41 +0200 Subject: [PATCH 150/266] Update requests to 2.32.4 (#146445) --- 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 f62c1c899ba..af9b0472bb1 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -61,7 +61,7 @@ pyspeex-noise==1.0.2 python-slugify==8.0.4 PyTurboJPEG==1.7.5 PyYAML==6.0.2 -requests==2.32.3 +requests==2.32.4 securetar==2025.2.1 SQLAlchemy==2.0.40 standard-aifc==3.13.0 diff --git a/pyproject.toml b/pyproject.toml index c31deb67dcf..52910c7f319 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -106,7 +106,7 @@ dependencies = [ # dependencies to stage 0. "PyTurboJPEG==1.7.5", "PyYAML==6.0.2", - "requests==2.32.3", + "requests==2.32.4", "securetar==2025.2.1", "SQLAlchemy==2.0.40", "standard-aifc==3.13.0", diff --git a/requirements.txt b/requirements.txt index c07d0e282a0..bff95490470 100644 --- a/requirements.txt +++ b/requirements.txt @@ -46,7 +46,7 @@ pyspeex-noise==1.0.2 python-slugify==8.0.4 PyTurboJPEG==1.7.5 PyYAML==6.0.2 -requests==2.32.3 +requests==2.32.4 securetar==2025.2.1 SQLAlchemy==2.0.40 standard-aifc==3.13.0 From bdbb74aff120b9f6efc234626ca4c564efd0fa28 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 10 Jun 2025 14:52:24 +0200 Subject: [PATCH 151/266] Return expected state in SmartThings water heater (#146449) --- .../components/smartthings/strings.json | 9 ----- .../components/smartthings/water_heater.py | 9 +++-- .../snapshots/test_water_heater.ambr | 36 +++++++++---------- .../smartthings/test_water_heater.py | 27 +++++++------- 4 files changed, 39 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/smartthings/strings.json b/homeassistant/components/smartthings/strings.json index 7b5edde2d10..8e972ac8aea 100644 --- a/homeassistant/components/smartthings/strings.json +++ b/homeassistant/components/smartthings/strings.json @@ -619,15 +619,6 @@ "keep_fresh_mode": { "name": "Keep fresh mode" } - }, - "water_heater": { - "water_heater": { - "state": { - "standard": "Standard", - "force": "Forced", - "power": "Power" - } - } } }, "issues": { diff --git a/homeassistant/components/smartthings/water_heater.py b/homeassistant/components/smartthings/water_heater.py index addbfed2ec4..4b1aaaa5549 100644 --- a/homeassistant/components/smartthings/water_heater.py +++ b/homeassistant/components/smartthings/water_heater.py @@ -10,6 +10,9 @@ from homeassistant.components.water_heater import ( DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, STATE_ECO, + STATE_HEAT_PUMP, + STATE_HIGH_DEMAND, + STATE_PERFORMANCE, WaterHeaterEntity, WaterHeaterEntityFeature, ) @@ -24,9 +27,9 @@ from .entity import SmartThingsEntity OPERATION_MAP_TO_HA: dict[str, str] = { "eco": STATE_ECO, - "std": "standard", - "force": "force", - "power": "power", + "std": STATE_HEAT_PUMP, + "force": STATE_HIGH_DEMAND, + "power": STATE_PERFORMANCE, } HA_TO_OPERATION_MAP = {v: k for k, v in OPERATION_MAP_TO_HA.items()} diff --git a/tests/components/smartthings/snapshots/test_water_heater.ambr b/tests/components/smartthings/snapshots/test_water_heater.ambr index 3e5afed3b86..d52400b9de2 100644 --- a/tests/components/smartthings/snapshots/test_water_heater.ambr +++ b/tests/components/smartthings/snapshots/test_water_heater.ambr @@ -10,9 +10,9 @@ 'operation_list': list([ 'off', 'eco', - 'standard', - 'power', - 'force', + 'heat_pump', + 'performance', + 'high_demand', ]), }), 'config_entry_id': , @@ -55,9 +55,9 @@ 'operation_list': list([ 'off', 'eco', - 'standard', - 'power', - 'force', + 'heat_pump', + 'performance', + 'high_demand', ]), 'operation_mode': 'off', 'supported_features': , @@ -84,8 +84,8 @@ 'operation_list': list([ 'off', 'eco', - 'standard', - 'force', + 'heat_pump', + 'high_demand', ]), }), 'config_entry_id': , @@ -128,8 +128,8 @@ 'operation_list': list([ 'off', 'eco', - 'standard', - 'force', + 'heat_pump', + 'high_demand', ]), 'operation_mode': 'off', 'supported_features': , @@ -156,9 +156,9 @@ 'operation_list': list([ 'off', 'eco', - 'standard', - 'power', - 'force', + 'heat_pump', + 'performance', + 'high_demand', ]), }), 'config_entry_id': , @@ -201,11 +201,11 @@ 'operation_list': list([ 'off', 'eco', - 'standard', - 'power', - 'force', + 'heat_pump', + 'performance', + 'high_demand', ]), - 'operation_mode': 'standard', + 'operation_mode': 'heat_pump', 'supported_features': , 'target_temp_high': 57, 'target_temp_low': 40, @@ -216,6 +216,6 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'standard', + 'state': 'heat_pump', }) # --- diff --git a/tests/components/smartthings/test_water_heater.py b/tests/components/smartthings/test_water_heater.py index a12280e5c92..30c85539d3a 100644 --- a/tests/components/smartthings/test_water_heater.py +++ b/tests/components/smartthings/test_water_heater.py @@ -20,6 +20,9 @@ from homeassistant.components.water_heater import ( SERVICE_SET_OPERATION_MODE, SERVICE_SET_TEMPERATURE, STATE_ECO, + STATE_HEAT_PUMP, + STATE_HIGH_DEMAND, + STATE_PERFORMANCE, WaterHeaterEntityFeature, ) from homeassistant.const import ( @@ -66,9 +69,9 @@ async def test_all_entities( ("operation_mode", "argument"), [ (STATE_ECO, "eco"), - ("standard", "std"), - ("force", "force"), - ("power", "power"), + (STATE_HEAT_PUMP, "std"), + (STATE_HIGH_DEMAND, "force"), + (STATE_PERFORMANCE, "power"), ], ) async def test_set_operation_mode( @@ -299,9 +302,9 @@ async def test_operation_list_update( ] == [ STATE_OFF, STATE_ECO, - "standard", - "power", - "force", + STATE_HEAT_PUMP, + STATE_PERFORMANCE, + STATE_HIGH_DEMAND, ] await trigger_update( @@ -318,8 +321,8 @@ async def test_operation_list_update( ] == [ STATE_OFF, STATE_ECO, - "force", - "power", + STATE_HIGH_DEMAND, + STATE_PERFORMANCE, ] @@ -332,7 +335,7 @@ async def test_current_operation_update( """Test state update.""" await setup_integration(hass, mock_config_entry) - assert hass.states.get("water_heater.warmepumpe").state == "standard" + assert hass.states.get("water_heater.warmepumpe").state == STATE_HEAT_PUMP await trigger_update( hass, @@ -356,7 +359,7 @@ async def test_switch_update( await setup_integration(hass, mock_config_entry) state = hass.states.get("water_heater.warmepumpe") - assert state.state == "standard" + assert state.state == STATE_HEAT_PUMP assert ( state.attributes[ATTR_SUPPORTED_FEATURES] == WaterHeaterEntityFeature.ON_OFF @@ -516,7 +519,7 @@ async def test_availability( """Test availability.""" await setup_integration(hass, mock_config_entry) - assert hass.states.get("water_heater.warmepumpe").state == "standard" + assert hass.states.get("water_heater.warmepumpe").state == STATE_HEAT_PUMP await trigger_health_update( hass, devices, "3810e5ad-5351-d9f9-12ff-000001200000", HealthStatus.OFFLINE @@ -528,7 +531,7 @@ async def test_availability( hass, devices, "3810e5ad-5351-d9f9-12ff-000001200000", HealthStatus.ONLINE ) - assert hass.states.get("water_heater.warmepumpe").state == "standard" + assert hass.states.get("water_heater.warmepumpe").state == STATE_HEAT_PUMP @pytest.mark.parametrize("device_fixture", ["da_sac_ehs_000002_sub"]) From fcd71931e75907740743fa532c744e9dec2dcc59 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Tue, 10 Jun 2025 17:04:22 +0100 Subject: [PATCH 152/266] Update wording deprecated system package integration repair (#146450) Co-authored-by: Martin Hjelmare --- homeassistant/components/homeassistant/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homeassistant/strings.json b/homeassistant/components/homeassistant/strings.json index 123e625d0fc..93b4105c702 100644 --- a/homeassistant/components/homeassistant/strings.json +++ b/homeassistant/components/homeassistant/strings.json @@ -20,11 +20,11 @@ }, "deprecated_system_packages_config_flow_integration": { "title": "The {integration_title} integration is being removed", - "description": "The {integration_title} integration is being removed as it requires additional system packages, which can't be installed on supported Home Assistant installations. Remove all \"{integration_title}\" config entries to fix this issue." + "description": "The {integration_title} integration is being removed as it depends on system packages that can only be installed on systems running a deprecated architecture. To resolve this, remove all \"{integration_title}\" config entries." }, "deprecated_system_packages_yaml_integration": { "title": "The {integration_title} integration is being removed", - "description": "The {integration_title} integration is being removed as it requires additional system packages, which can't be installed on supported Home Assistant installations. Remove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + "description": "The {integration_title} integration is being removed as it depends on system packages that can only be installed on systems running a deprecated architecture. To resolve this, remove the {domain} entry from your configuration.yaml file and restart Home Assistant." }, "historic_currency": { "title": "The configured currency is no longer in use", From 1040646610732cc38a4bc3d87fd9eecbc4aec6ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luca=20Schr=C3=B6der?= Date: Tue, 10 Jun 2025 16:20:35 +0200 Subject: [PATCH 153/266] Update caldav to 1.6.0 (#146456) Fixes #140798 --- homeassistant/components/caldav/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/caldav/manifest.json b/homeassistant/components/caldav/manifest.json index 5c1334c8029..d0e0bd0b1d0 100644 --- a/homeassistant/components/caldav/manifest.json +++ b/homeassistant/components/caldav/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/caldav", "iot_class": "cloud_polling", "loggers": ["caldav", "vobject"], - "requirements": ["caldav==1.3.9", "icalendar==6.1.0"] + "requirements": ["caldav==1.6.0", "icalendar==6.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index dd84a358fd4..a459bafdbf3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -698,7 +698,7 @@ buienradar==1.0.6 cached-ipaddress==0.10.0 # homeassistant.components.caldav -caldav==1.3.9 +caldav==1.6.0 # homeassistant.components.cisco_mobility_express ciscomobilityexpress==0.3.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d0c4252f456..f0b1e22519b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -616,7 +616,7 @@ buienradar==1.0.6 cached-ipaddress==0.10.0 # homeassistant.components.caldav -caldav==1.3.9 +caldav==1.6.0 # homeassistant.components.coinbase coinbase-advanced-py==1.2.2 From 1d91ca5716854b2e28dfc46e9a138fb792be8627 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 10 Jun 2025 17:37:21 +0200 Subject: [PATCH 154/266] Bump pySmartThings to 3.2.4 (#146459) --- 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 180d4eebed1..481048c3bdb 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.3"] + "requirements": ["pysmartthings==3.2.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index a459bafdbf3..67b7173fe98 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2341,7 +2341,7 @@ pysmappee==0.2.29 pysmarlaapi==0.8.2 # homeassistant.components.smartthings -pysmartthings==3.2.3 +pysmartthings==3.2.4 # homeassistant.components.smarty pysmarty2==0.10.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f0b1e22519b..42d106778b0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1941,7 +1941,7 @@ pysmappee==0.2.29 pysmarlaapi==0.8.2 # homeassistant.components.smartthings -pysmartthings==3.2.3 +pysmartthings==3.2.4 # homeassistant.components.smarty pysmarty2==0.10.2 From 18e1a26da1987c57c56d4b96398cb43761562970 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 10 Jun 2025 17:31:30 +0200 Subject: [PATCH 155/266] Catch exception before retrying in AirGradient (#146460) --- .../components/airgradient/coordinator.py | 13 ++++++++++--- tests/components/airgradient/test_init.py | 15 +++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airgradient/coordinator.py b/homeassistant/components/airgradient/coordinator.py index 7484c7e85a9..9ee103b3a90 100644 --- a/homeassistant/components/airgradient/coordinator.py +++ b/homeassistant/components/airgradient/coordinator.py @@ -51,9 +51,16 @@ class AirGradientCoordinator(DataUpdateCoordinator[AirGradientData]): async def _async_setup(self) -> None: """Set up the coordinator.""" - self._current_version = ( - await self.client.get_current_measures() - ).firmware_version + try: + self._current_version = ( + await self.client.get_current_measures() + ).firmware_version + except AirGradientError as error: + raise UpdateFailed( + translation_domain=DOMAIN, + translation_key="update_error", + translation_placeholders={"error": str(error)}, + ) from error async def _async_update_data(self) -> AirGradientData: try: diff --git a/tests/components/airgradient/test_init.py b/tests/components/airgradient/test_init.py index a253cb2888a..5732cd526f6 100644 --- a/tests/components/airgradient/test_init.py +++ b/tests/components/airgradient/test_init.py @@ -3,10 +3,12 @@ from datetime import timedelta from unittest.mock import AsyncMock +from airgradient import AirGradientError from freezegun.api import FrozenDateTimeFactory from syrupy.assertion import SnapshotAssertion from homeassistant.components.airgradient.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr @@ -54,3 +56,16 @@ async def test_new_firmware_version( ) assert device_entry is not None assert device_entry.sw_version == "3.1.2" + + +async def test_setup_retry( + hass: HomeAssistant, + mock_airgradient_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test retrying setup.""" + mock_airgradient_client.get_current_measures.side_effect = AirGradientError() + + await setup_integration(hass, mock_config_entry) + + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY From 49646210144aba77abc71f5c86c0d4707a4c8ad1 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 10 Jun 2025 19:28:48 +0200 Subject: [PATCH 156/266] Fix incorrect categories handling in holiday (#146470) --- homeassistant/components/holiday/calendar.py | 16 ++-- tests/components/holiday/test_calendar.py | 80 +++++++++++++++++++- 2 files changed, 87 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/holiday/calendar.py b/homeassistant/components/holiday/calendar.py index 1c01319129b..c5b67b7d555 100644 --- a/homeassistant/components/holiday/calendar.py +++ b/homeassistant/components/holiday/calendar.py @@ -25,17 +25,12 @@ def _get_obj_holidays_and_language( selected_categories: list[str] | None, ) -> tuple[HolidayBase, str]: """Get the object for the requested country and year.""" - if selected_categories is None: - categories = [PUBLIC] - else: - categories = [PUBLIC, *selected_categories] - obj_holidays = country_holidays( country, subdiv=province, years={dt_util.now().year, dt_util.now().year + 1}, language=language, - categories=categories, + categories=selected_categories, ) if language == "en": for lang in obj_holidays.supported_languages: @@ -45,7 +40,7 @@ def _get_obj_holidays_and_language( subdiv=province, years={dt_util.now().year, dt_util.now().year + 1}, language=lang, - categories=categories, + categories=selected_categories, ) language = lang break @@ -59,7 +54,7 @@ def _get_obj_holidays_and_language( subdiv=province, years={dt_util.now().year, dt_util.now().year + 1}, language=default_language, - categories=categories, + categories=selected_categories, ) language = default_language @@ -77,6 +72,11 @@ async def async_setup_entry( categories: list[str] | None = config_entry.options.get(CONF_CATEGORIES) language = hass.config.language + if categories is None: + categories = [PUBLIC] + else: + categories = [PUBLIC, *categories] + obj_holidays, language = await hass.async_add_executor_job( _get_obj_holidays_and_language, country, province, language, categories ) diff --git a/tests/components/holiday/test_calendar.py b/tests/components/holiday/test_calendar.py index 6733d38442b..463f8645647 100644 --- a/tests/components/holiday/test_calendar.py +++ b/tests/components/holiday/test_calendar.py @@ -3,13 +3,18 @@ from datetime import datetime, timedelta from freezegun.api import FrozenDateTimeFactory +from holidays import CATHOLIC import pytest from homeassistant.components.calendar import ( DOMAIN as CALENDAR_DOMAIN, SERVICE_GET_EVENTS, ) -from homeassistant.components.holiday.const import CONF_PROVINCE, DOMAIN +from homeassistant.components.holiday.const import ( + CONF_CATEGORIES, + CONF_PROVINCE, + DOMAIN, +) from homeassistant.const import CONF_COUNTRY from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -353,3 +358,76 @@ async def test_language_not_exist( ] } } + + +async def test_categories( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, +) -> None: + """Test if there is no next event.""" + await hass.config.async_set_time_zone("Europe/Berlin") + zone = await dt_util.async_get_time_zone("Europe/Berlin") + freezer.move_to(datetime(2025, 8, 14, 12, tzinfo=zone)) + + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_COUNTRY: "DE", + CONF_PROVINCE: "BY", + }, + options={ + CONF_CATEGORIES: [CATHOLIC], + }, + title="Germany", + ) + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + response = await hass.services.async_call( + CALENDAR_DOMAIN, + SERVICE_GET_EVENTS, + { + "entity_id": "calendar.germany", + "end_date_time": dt_util.now() + timedelta(days=2), + }, + blocking=True, + return_response=True, + ) + assert response == { + "calendar.germany": { + "events": [ + { + "start": "2025-08-15", + "end": "2025-08-16", + "summary": "Assumption Day", + "location": "Germany", + } + ] + } + } + + freezer.move_to(datetime(2025, 12, 23, 12, tzinfo=zone)) + response = await hass.services.async_call( + CALENDAR_DOMAIN, + SERVICE_GET_EVENTS, + { + "entity_id": "calendar.germany", + "end_date_time": dt_util.now() + timedelta(days=2), + }, + blocking=True, + return_response=True, + ) + assert response == { + "calendar.germany": { + "events": [ + { + "start": "2025-12-25", + "end": "2025-12-26", + "summary": "Christmas Day", + "location": "Germany", + } + ] + } + } From 39962a3f48d4d2b5b3f852355044a5b9908b86a5 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Tue, 10 Jun 2025 20:18:19 +0300 Subject: [PATCH 157/266] Avoid closing shared aiohttp session in Vodafone Station (#146471) --- .../components/vodafone_station/__init__.py | 1 - .../vodafone_station/config_flow.py | 1 - .../vodafone_station/coordinator.py | 47 +++++++++---------- 3 files changed, 22 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/vodafone_station/__init__.py b/homeassistant/components/vodafone_station/__init__.py index 5efc33ca882..17b0fe6e501 100644 --- a/homeassistant/components/vodafone_station/__init__.py +++ b/homeassistant/components/vodafone_station/__init__.py @@ -37,7 +37,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: VodafoneConfigEntry) -> if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): coordinator = entry.runtime_data await coordinator.api.logout() - await coordinator.api.close() return unload_ok diff --git a/homeassistant/components/vodafone_station/config_flow.py b/homeassistant/components/vodafone_station/config_flow.py index b69078b8ce6..c330a93a1a8 100644 --- a/homeassistant/components/vodafone_station/config_flow.py +++ b/homeassistant/components/vodafone_station/config_flow.py @@ -48,7 +48,6 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, await api.login() finally: await api.logout() - await api.close() return {"title": data[CONF_HOST]} diff --git a/homeassistant/components/vodafone_station/coordinator.py b/homeassistant/components/vodafone_station/coordinator.py index 846d4b042c0..57d39151160 100644 --- a/homeassistant/components/vodafone_station/coordinator.py +++ b/homeassistant/components/vodafone_station/coordinator.py @@ -117,32 +117,29 @@ class VodafoneStationRouter(DataUpdateCoordinator[UpdateCoordinatorDataType]): async def _async_update_data(self) -> UpdateCoordinatorDataType: """Update router data.""" _LOGGER.debug("Polling Vodafone Station host: %s", self._host) + try: - try: - await self.api.login() - raw_data_devices = await self.api.get_devices_data() - data_sensors = await self.api.get_sensor_data() - await self.api.logout() - except exceptions.CannotAuthenticate as err: - raise ConfigEntryAuthFailed( - translation_domain=DOMAIN, - translation_key="cannot_authenticate", - translation_placeholders={"error": repr(err)}, - ) from err - except ( - exceptions.CannotConnect, - exceptions.AlreadyLogged, - exceptions.GenericLoginError, - JSONDecodeError, - ) as err: - raise UpdateFailed( - translation_domain=DOMAIN, - translation_key="update_failed", - translation_placeholders={"error": repr(err)}, - ) from err - except (ConfigEntryAuthFailed, UpdateFailed): - await self.api.close() - raise + await self.api.login() + raw_data_devices = await self.api.get_devices_data() + data_sensors = await self.api.get_sensor_data() + await self.api.logout() + except exceptions.CannotAuthenticate as err: + raise ConfigEntryAuthFailed( + translation_domain=DOMAIN, + translation_key="cannot_authenticate", + translation_placeholders={"error": repr(err)}, + ) from err + except ( + exceptions.CannotConnect, + exceptions.AlreadyLogged, + exceptions.GenericLoginError, + JSONDecodeError, + ) as err: + raise UpdateFailed( + translation_domain=DOMAIN, + translation_key="update_failed", + translation_placeholders={"error": repr(err)}, + ) from err utc_point_in_time = dt_util.utcnow() data_devices = { From bf8ef0a767de8ee0696eb1ad18d4dd0b93a3eda8 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Tue, 10 Jun 2025 20:28:37 +0300 Subject: [PATCH 158/266] Fix EntityCategory for binary_sensor platform in Amazon Devices (#146472) * Fix EntityCategory for binary_sensor platform in Amazon Devices * update snapshots --- homeassistant/components/amazon_devices/binary_sensor.py | 3 +++ .../amazon_devices/snapshots/test_binary_sensor.ambr | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/amazon_devices/binary_sensor.py b/homeassistant/components/amazon_devices/binary_sensor.py index 2e41983dda4..ab1fadc7548 100644 --- a/homeassistant/components/amazon_devices/binary_sensor.py +++ b/homeassistant/components/amazon_devices/binary_sensor.py @@ -13,6 +13,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) +from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback @@ -34,10 +35,12 @@ BINARY_SENSORS: Final = ( AmazonBinarySensorEntityDescription( key="online", device_class=BinarySensorDeviceClass.CONNECTIVITY, + entity_category=EntityCategory.DIAGNOSTIC, is_on_fn=lambda _device: _device.online, ), AmazonBinarySensorEntityDescription( key="bluetooth", + entity_category=EntityCategory.DIAGNOSTIC, translation_key="bluetooth", is_on_fn=lambda _device: _device.bluetooth_state, ), diff --git a/tests/components/amazon_devices/snapshots/test_binary_sensor.ambr b/tests/components/amazon_devices/snapshots/test_binary_sensor.ambr index 0d3a5252a73..e914541d19c 100644 --- a/tests/components/amazon_devices/snapshots/test_binary_sensor.ambr +++ b/tests/components/amazon_devices/snapshots/test_binary_sensor.ambr @@ -11,7 +11,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'binary_sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'binary_sensor.echo_test_bluetooth', 'has_entity_name': True, 'hidden_by': None, @@ -59,7 +59,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'binary_sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'binary_sensor.echo_test_connectivity', 'has_entity_name': True, 'hidden_by': None, From 8949a595fe026cd54321b499df982e966ea84248 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 10 Jun 2025 17:45:26 +0000 Subject: [PATCH 159/266] Bump version to 2025.6.0b6 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 25d722ea685..b07549d387f 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 = 6 -PATCH_VERSION: Final = "0b5" +PATCH_VERSION: Final = "0b6" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index 52910c7f319..58ad46b63e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.6.0b5" +version = "2025.6.0b6" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From 35580c0849e09ba874c2e792d3feb58e7632b33e Mon Sep 17 00:00:00 2001 From: hahn-th <15319212+hahn-th@users.noreply.github.com> Date: Tue, 10 Jun 2025 20:44:06 +0200 Subject: [PATCH 160/266] Bump homematicip to 2.0.4 (#144096) * Bump to 2.0.2 with all necessary changes * bump to prerelease * add addiional tests * Bump to homematicip 2.0.3 * do not delete device * Setup BRAND_SWITCH_MEASURING as light * bump to 2.0.4 * refactor test_remove_obsolete_entities * move test * use const from homematicip lib --- .../components/homematicip_cloud/hap.py | 16 +++++++++++++++ .../components/homematicip_cloud/light.py | 11 ++++++---- .../homematicip_cloud/manifest.json | 2 +- .../components/homematicip_cloud/sensor.py | 13 ++---------- .../components/homematicip_cloud/switch.py | 15 ++++++-------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/homematicip_cloud/test_hap.py | 20 +++++++++++++++++++ 8 files changed, 54 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index 86630c2896c..f3681a89110 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -112,6 +112,7 @@ class HomematicipHAP: self.config_entry = config_entry 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 @@ -218,6 +219,8 @@ class HomematicipHAP: try: await self.home.get_current_state_async() hmip_events = self.home.enable_events() + self.home.set_on_connected_handler(self.ws_connected_handler) + self.home.set_on_disconnected_handler(self.ws_disconnected_handler) tries = 0 await hmip_events except HmipConnectionError: @@ -267,6 +270,18 @@ class HomematicipHAP: "Reset connection to access point id %s", self.config_entry.unique_id ) + async def ws_connected_handler(self) -> None: + """Handle websocket connected.""" + _LOGGER.debug("WebSocket connection to HomematicIP established") + if self._ws_connection_closed.is_set(): + await self.get_state() + self._ws_connection_closed.clear() + + async def ws_disconnected_handler(self) -> None: + """Handle websocket disconnection.""" + _LOGGER.warning("WebSocket connection to HomematicIP closed") + self._ws_connection_closed.set() + async def get_hap( self, hass: HomeAssistant, @@ -290,6 +305,7 @@ class HomematicipHAP: raise HmipcConnectionError from err home.on_update(self.async_update) home.on_create(self.async_create_entity) + hass.loop.create_task(self.async_connect()) return home diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py index 855f5851d73..d5175e6e647 100644 --- a/homeassistant/components/homematicip_cloud/light.py +++ b/homeassistant/components/homematicip_cloud/light.py @@ -4,16 +4,16 @@ from __future__ import annotations from typing import Any -from homematicip.base.enums import OpticalSignalBehaviour, RGBColorState +from homematicip.base.enums import DeviceType, OpticalSignalBehaviour, RGBColorState from homematicip.base.functionalChannels import NotificationLightChannel from homematicip.device import ( BrandDimmer, - BrandSwitchMeasuring, BrandSwitchNotificationLight, Dimmer, DinRailDimmer3, FullFlushDimmer, PluggableDimmer, + SwitchMeasuring, WiredDimmer3, ) from packaging.version import Version @@ -44,9 +44,12 @@ async def async_setup_entry( hap = config_entry.runtime_data entities: list[HomematicipGenericEntity] = [] for device in hap.home.devices: - if isinstance(device, BrandSwitchMeasuring): + if ( + isinstance(device, SwitchMeasuring) + and getattr(device, "deviceType", None) == DeviceType.BRAND_SWITCH_MEASURING + ): entities.append(HomematicipLightMeasuring(hap, device)) - elif isinstance(device, BrandSwitchNotificationLight): + if isinstance(device, BrandSwitchNotificationLight): device_version = Version(device.firmwareVersion) entities.append(HomematicipLight(hap, device)) diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index 15bc24c110f..fc4a1cb831f 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.1.1"] + "requirements": ["homematicip==2.0.4"] } diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index 4f43e6d6ca7..13f3694de7a 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -11,12 +11,10 @@ from homematicip.base.functionalChannels import ( FunctionalChannel, ) from homematicip.device import ( - BrandSwitchMeasuring, EnergySensorsInterface, FloorTerminalBlock6, FloorTerminalBlock10, FloorTerminalBlock12, - FullFlushSwitchMeasuring, HeatingThermostat, HeatingThermostatCompact, HeatingThermostatEvo, @@ -26,9 +24,9 @@ from homematicip.device import ( MotionDetectorOutdoor, MotionDetectorPushButton, PassageDetector, - PlugableSwitchMeasuring, PresenceDetectorIndoor, RoomControlDeviceAnalog, + SwitchMeasuring, TemperatureDifferenceSensor2, TemperatureHumiditySensorDisplay, TemperatureHumiditySensorOutdoor, @@ -143,14 +141,7 @@ async def async_setup_entry( ), ): entities.append(HomematicipIlluminanceSensor(hap, device)) - if isinstance( - device, - ( - PlugableSwitchMeasuring, - BrandSwitchMeasuring, - FullFlushSwitchMeasuring, - ), - ): + if isinstance(device, SwitchMeasuring): entities.append(HomematicipPowerSensor(hap, device)) entities.append(HomematicipEnergySensor(hap, device)) if isinstance(device, (WeatherSensor, WeatherSensorPlus, WeatherSensorPro)): diff --git a/homeassistant/components/homematicip_cloud/switch.py b/homeassistant/components/homematicip_cloud/switch.py index 4927d9a32df..66a40229c7e 100644 --- a/homeassistant/components/homematicip_cloud/switch.py +++ b/homeassistant/components/homematicip_cloud/switch.py @@ -4,20 +4,19 @@ from __future__ import annotations from typing import Any +from homematicip.base.enums import DeviceType from homematicip.device import ( BrandSwitch2, - BrandSwitchMeasuring, DinRailSwitch, DinRailSwitch4, FullFlushInputSwitch, - FullFlushSwitchMeasuring, HeatingSwitch2, MultiIOBox, OpenCollector8Module, PlugableSwitch, - PlugableSwitchMeasuring, PrintedCircuitBoardSwitch2, PrintedCircuitBoardSwitchBattery, + SwitchMeasuring, WiredSwitch8, ) from homematicip.group import ExtendedLinkedSwitchingGroup, SwitchingGroup @@ -43,12 +42,10 @@ async def async_setup_entry( if isinstance(group, (ExtendedLinkedSwitchingGroup, SwitchingGroup)) ] for device in hap.home.devices: - if isinstance(device, BrandSwitchMeasuring): - # BrandSwitchMeasuring inherits PlugableSwitchMeasuring - # This entity is implemented in the light platform and will - # not be added in the switch platform - pass - elif isinstance(device, (PlugableSwitchMeasuring, FullFlushSwitchMeasuring)): + if ( + isinstance(device, SwitchMeasuring) + and getattr(device, "deviceType", None) != DeviceType.BRAND_SWITCH_MEASURING + ): entities.append(HomematicipSwitchMeasuring(hap, device)) elif isinstance(device, WiredSwitch8): entities.extend( diff --git a/requirements_all.txt b/requirements_all.txt index 67b7173fe98..98b43cb4635 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1170,7 +1170,7 @@ home-assistant-frontend==20250531.0 home-assistant-intents==2025.5.28 # homeassistant.components.homematicip_cloud -homematicip==2.0.1.1 +homematicip==2.0.4 # homeassistant.components.horizon horimote==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 42d106778b0..23ace366641 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1016,7 +1016,7 @@ home-assistant-frontend==20250531.0 home-assistant-intents==2025.5.28 # homeassistant.components.homematicip_cloud -homematicip==2.0.1.1 +homematicip==2.0.4 # homeassistant.components.remember_the_milk httplib2==0.20.4 diff --git a/tests/components/homematicip_cloud/test_hap.py b/tests/components/homematicip_cloud/test_hap.py index 13aaa4d83ba..94d6f9d5dd6 100644 --- a/tests/components/homematicip_cloud/test_hap.py +++ b/tests/components/homematicip_cloud/test_hap.py @@ -231,3 +231,23 @@ async def test_auth_create_exception(hass: HomeAssistant, simple_mock_auth) -> N ), ): assert not await hmip_auth.get_auth(hass, HAPID, HAPPIN) + + +async def test_get_state_after_disconnect( + hass: HomeAssistant, hmip_config_entry: MockConfigEntry, simple_mock_home +) -> None: + """Test get state after disconnect.""" + hass.config.components.add(DOMAIN) + hap = HomematicipHAP(hass, hmip_config_entry) + assert hap + + with patch.object(hap, "get_state") as mock_get_state: + assert not hap._ws_connection_closed.is_set() + + await hap.ws_connected_handler() + mock_get_state.assert_not_called() + + await hap.ws_disconnected_handler() + assert hap._ws_connection_closed.is_set() + await hap.ws_connected_handler() + mock_get_state.assert_called_once() From 63e49c5d3c70bab6e98a3bb444857bd056f39c08 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 11 Jun 2025 10:31:08 +0200 Subject: [PATCH 161/266] Explain Nest setup (#146217) --- homeassistant/components/nest/config_flow.py | 8 -------- homeassistant/components/nest/strings.json | 3 +++ tests/components/nest/test_config_flow.py | 8 ++++++++ 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/nest/config_flow.py b/homeassistant/components/nest/config_flow.py index 1513a039407..0b249db7a4b 100644 --- a/homeassistant/components/nest/config_flow.py +++ b/homeassistant/components/nest/config_flow.py @@ -31,7 +31,6 @@ from homeassistant.helpers.selector import ( SelectSelectorConfig, SelectSelectorMode, ) -from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from homeassistant.util import get_random_string from . import api @@ -441,10 +440,3 @@ class NestFlowHandler( if self._structure_config_title: title = self._structure_config_title return self.async_create_entry(title=title, data=self._data) - - async def async_step_dhcp( - self, discovery_info: DhcpServiceInfo - ) -> ConfigFlowResult: - """Handle a flow initialized by discovery.""" - await self._async_handle_discovery_without_unique_id() - return await self.async_step_user() diff --git a/homeassistant/components/nest/strings.json b/homeassistant/components/nest/strings.json index 4a8689ff04c..5146d04af0b 100644 --- a/homeassistant/components/nest/strings.json +++ b/homeassistant/components/nest/strings.json @@ -47,6 +47,9 @@ "reauth_confirm": { "title": "[%key:common::config_flow::title::reauth%]", "description": "The Nest integration needs to re-authenticate your account" + }, + "oauth_discovery": { + "description": "Home Assistant has found a Google Nest device on your network. Be aware that the set up of Google Nest is more complicated than most other integrations and requires setting up a Nest Device Access project which **requires paying Google a US $5 fee**. Press **Submit** to continue setting up Google Nest." } }, "error": { diff --git a/tests/components/nest/test_config_flow.py b/tests/components/nest/test_config_flow.py index 3f369f3e127..67364aff412 100644 --- a/tests/components/nest/test_config_flow.py +++ b/tests/components/nest/test_config_flow.py @@ -995,6 +995,10 @@ async def test_dhcp_discovery( ) await hass.async_block_till_done() assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "oauth_discovery" + + result = await oauth.async_configure(result, {}) + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "create_cloud_project" result = await oauth.async_configure(result, {}) @@ -1033,6 +1037,10 @@ async def test_dhcp_discovery_with_creds( ) await hass.async_block_till_done() assert result.get("type") is FlowResultType.FORM + assert result.get("step_id") == "oauth_discovery" + + result = await oauth.async_configure(result, {}) + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "cloud_project" result = await oauth.async_configure(result, {"cloud_project_id": CLOUD_PROJECT_ID}) From 4147211f948195fb78fbd3d804a70839a9a3b42c Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Wed, 11 Jun 2025 05:58:07 -0400 Subject: [PATCH 162/266] Add color_temp_kelvin to set_temperature action variables (#146448) --- homeassistant/components/template/light.py | 4 +++- tests/components/template/test_light.py | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index 9fc935bf0ee..c852ee1808d 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -524,8 +524,10 @@ class AbstractTemplateLight(AbstractTemplateEntity, LightEntity): ATTR_COLOR_TEMP_KELVIN in kwargs and (script := CONF_TEMPERATURE_ACTION) in self._action_scripts ): + kelvin = kwargs[ATTR_COLOR_TEMP_KELVIN] + common_params[ATTR_COLOR_TEMP_KELVIN] = kelvin common_params["color_temp"] = color_util.color_temperature_kelvin_to_mired( - kwargs[ATTR_COLOR_TEMP_KELVIN] + kelvin ) return (script, common_params) diff --git a/tests/components/template/test_light.py b/tests/components/template/test_light.py index f240c2412e0..eaa1708aea7 100644 --- a/tests/components/template/test_light.py +++ b/tests/components/template/test_light.py @@ -79,6 +79,7 @@ OPTIMISTIC_COLOR_TEMP_LIGHT_CONFIG = { "action": "set_temperature", "caller": "{{ this.entity_id }}", "color_temp": "{{color_temp}}", + "color_temp_kelvin": "{{color_temp_kelvin}}", }, }, } @@ -1535,6 +1536,7 @@ async def test_temperature_action_no_template( assert calls[-1].data["action"] == "set_temperature" assert calls[-1].data["caller"] == "light.test_template_light" assert calls[-1].data["color_temp"] == 345 + assert calls[-1].data["color_temp_kelvin"] == 2898 state = hass.states.get("light.test_template_light") assert state is not None From 43e16bb913e51927e869290becf06ae0219a79d7 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 11 Jun 2025 11:35:14 +0200 Subject: [PATCH 163/266] Split deprecated system issue in 2 places (#146453) --- homeassistant/components/hassio/__init__.py | 67 +++- .../components/homeassistant/__init__.py | 95 ++---- tests/components/hassio/test_init.py | 290 +++++++++++++++++- tests/components/homeassistant/test_init.py | 80 +---- 4 files changed, 391 insertions(+), 141 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index eeeedff00bb..041877e3944 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -37,6 +37,7 @@ from homeassistant.helpers import ( config_validation as cv, device_registry as dr, discovery_flow, + issue_registry as ir, ) from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.deprecation import ( @@ -51,9 +52,11 @@ from homeassistant.helpers.hassio import ( get_supervisor_ip as _get_supervisor_ip, is_hassio as _is_hassio, ) +from homeassistant.helpers.issue_registry import IssueSeverity from homeassistant.helpers.service_info.hassio import ( HassioServiceInfo as _HassioServiceInfo, ) +from homeassistant.helpers.system_info import async_get_system_info from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from homeassistant.util.async_ import create_eager_task @@ -109,7 +112,7 @@ from .coordinator import ( get_core_info, # noqa: F401 get_core_stats, # noqa: F401 get_host_info, # noqa: F401 - get_info, # noqa: F401 + get_info, get_issues_info, # noqa: F401 get_os_info, get_supervisor_info, # noqa: F401 @@ -168,6 +171,11 @@ SERVICE_RESTORE_PARTIAL = "restore_partial" VALID_ADDON_SLUG = vol.Match(re.compile(r"^[-_.A-Za-z0-9]+$")) +DEPRECATION_URL = ( + "https://www.home-assistant.io/blog/2025/05/22/" + "deprecating-core-and-supervised-installation-methods-and-32-bit-systems/" +) + def valid_addon(value: Any) -> str: """Validate value is a valid addon slug.""" @@ -546,6 +554,63 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data[ADDONS_COORDINATOR] = coordinator + system_info = await async_get_system_info(hass) + + def deprecated_setup_issue() -> None: + os_info = get_os_info(hass) + info = get_info(hass) + if os_info is None or info is None: + return + is_haos = info.get("hassos") is not None + arch = system_info["arch"] + board = os_info.get("board") + supported_board = board in {"rpi3", "rpi4", "tinker", "odroid-xu4", "rpi2"} + if is_haos and arch == "armv7" and supported_board: + issue_id = "deprecated_os_" + if board in {"rpi3", "rpi4"}: + issue_id += "aarch64" + elif board in {"tinker", "odroid-xu4", "rpi2"}: + issue_id += "armv7" + ir.async_create_issue( + hass, + "homeassistant", + issue_id, + breaks_in_ha_version="2025.12.0", + learn_more_url=DEPRECATION_URL, + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key=issue_id, + translation_placeholders={ + "installation_guide": "https://www.home-assistant.io/installation/", + }, + ) + deprecated_architecture = False + if arch in {"i386", "armhf"} or (arch == "armv7" and not supported_board): + deprecated_architecture = True + if not is_haos or deprecated_architecture: + issue_id = "deprecated" + if not is_haos: + issue_id += "_method" + if deprecated_architecture: + issue_id += "_architecture" + ir.async_create_issue( + hass, + "homeassistant", + issue_id, + breaks_in_ha_version="2025.12.0", + learn_more_url=DEPRECATION_URL, + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key=issue_id, + translation_placeholders={ + "installation_type": "OS" if is_haos else "Supervised", + "arch": arch, + }, + ) + listener() + + listener = coordinator.async_add_listener(deprecated_setup_issue) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/homeassistant/__init__.py b/homeassistant/components/homeassistant/__init__.py index 5f012c6a054..1433358b568 100644 --- a/homeassistant/components/homeassistant/__init__.py +++ b/homeassistant/components/homeassistant/__init__.py @@ -4,7 +4,7 @@ import asyncio from collections.abc import Callable, Coroutine import itertools as it import logging -from typing import TYPE_CHECKING, Any +from typing import Any import voluptuous as vol @@ -38,7 +38,6 @@ from homeassistant.helpers import ( restore_state, ) from homeassistant.helpers.entity_component import async_update_entity -from homeassistant.helpers.importlib import async_import_module from homeassistant.helpers.issue_registry import IssueSeverity from homeassistant.helpers.service import ( async_extract_config_entry_ids, @@ -402,46 +401,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: info = await async_get_system_info(hass) installation_type = info["installation_type"][15:] - deprecated_method = installation_type in { - "Core", - "Supervised", - } - arch = info["arch"] - if arch == "armv7": - if installation_type == "OS": - # Local import to avoid circular dependencies - # We use the import helper because hassio - # may not be loaded yet and we don't want to - # do blocking I/O in the event loop to import it. - if TYPE_CHECKING: - # pylint: disable-next=import-outside-toplevel - from homeassistant.components import hassio - else: - hassio = await async_import_module( - hass, "homeassistant.components.hassio" - ) - os_info = hassio.get_os_info(hass) - assert os_info is not None - issue_id = "deprecated_os_" - board = os_info.get("board") - if board in {"rpi3", "rpi4"}: - issue_id += "aarch64" - elif board in {"tinker", "odroid-xu4", "rpi2"}: - issue_id += "armv7" - ir.async_create_issue( - hass, - DOMAIN, - issue_id, - breaks_in_ha_version="2025.12.0", - learn_more_url=DEPRECATION_URL, - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key=issue_id, - translation_placeholders={ - "installation_guide": "https://www.home-assistant.io/installation/", - }, - ) - elif installation_type == "Container": + if installation_type in {"Core", "Container"}: + deprecated_method = installation_type == "Core" + arch = info["arch"] + if arch == "armv7" and installation_type == "Container": ir.async_create_issue( hass, DOMAIN, @@ -452,29 +415,31 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: severity=IssueSeverity.WARNING, translation_key="deprecated_container_armv7", ) - deprecated_architecture = False - if arch in {"i386", "armhf"} or (arch == "armv7" and deprecated_method): - deprecated_architecture = True - if deprecated_method or deprecated_architecture: - issue_id = "deprecated" - if deprecated_method: - issue_id += "_method" - if deprecated_architecture: - issue_id += "_architecture" - ir.async_create_issue( - hass, - DOMAIN, - issue_id, - breaks_in_ha_version="2025.12.0", - learn_more_url=DEPRECATION_URL, - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key=issue_id, - translation_placeholders={ - "installation_type": installation_type, - "arch": arch, - }, - ) + deprecated_architecture = False + if arch in {"i386", "armhf"} or ( + arch == "armv7" and installation_type != "Container" + ): + deprecated_architecture = True + if deprecated_method or deprecated_architecture: + issue_id = "deprecated" + if deprecated_method: + issue_id += "_method" + if deprecated_architecture: + issue_id += "_architecture" + ir.async_create_issue( + hass, + DOMAIN, + issue_id, + breaks_in_ha_version="2025.12.0", + learn_more_url=DEPRECATION_URL, + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key=issue_id, + translation_placeholders={ + "installation_type": installation_type, + "arch": arch, + }, + ) return True diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index d34aed608fb..f74ed852a49 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -8,6 +8,7 @@ from unittest.mock import AsyncMock, patch from aiohasupervisor import SupervisorError from aiohasupervisor.models import AddonsStats +from freezegun.api import FrozenDateTimeFactory import pytest from voluptuous import Invalid @@ -23,10 +24,13 @@ from homeassistant.components.hassio import ( is_hassio as deprecated_is_hassio, ) from homeassistant.components.hassio.config import STORAGE_KEY -from homeassistant.components.hassio.const import REQUEST_REFRESH_DELAY +from homeassistant.components.hassio.const import ( + HASSIO_UPDATE_INTERVAL, + REQUEST_REFRESH_DELAY, +) from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import device_registry as dr, issue_registry as ir from homeassistant.helpers.hassio import is_hassio from homeassistant.helpers.service_info.hassio import HassioServiceInfo from homeassistant.setup import async_setup_component @@ -1140,3 +1144,285 @@ def test_deprecated_constants( replacement, "2025.11", ) + + +@pytest.mark.parametrize( + ("board", "issue_id"), + [ + ("rpi3", "deprecated_os_aarch64"), + ("rpi4", "deprecated_os_aarch64"), + ("tinker", "deprecated_os_armv7"), + ("odroid-xu4", "deprecated_os_armv7"), + ("rpi2", "deprecated_os_armv7"), + ], +) +async def test_deprecated_installation_issue_aarch64( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, + freezer: FrozenDateTimeFactory, + board: str, + issue_id: str, +) -> None: + """Test deprecated installation issue.""" + with ( + patch.dict(os.environ, MOCK_ENVIRON), + patch( + "homeassistant.components.hassio.async_get_system_info", + return_value={ + "installation_type": "Home Assistant OS", + "arch": "armv7", + }, + ), + patch( + "homeassistant.components.homeassistant.async_get_system_info", + return_value={ + "installation_type": "Home Assistant OS", + "arch": "armv7", + }, + ), + patch( + "homeassistant.components.hassio.get_os_info", return_value={"board": board} + ), + patch( + "homeassistant.components.hassio.get_info", return_value={"hassos": True} + ), + patch("homeassistant.components.hardware.async_setup", return_value=True), + ): + assert await async_setup_component(hass, "homeassistant", {}) + config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + freezer.tick(REQUEST_REFRESH_DELAY) + async_fire_time_changed(hass) + await hass.async_block_till_done() + await hass.services.async_call( + "homeassistant", + "update_entity", + { + "entity_id": [ + "update.home_assistant_core_update", + "update.home_assistant_supervisor_update", + ] + }, + blocking=True, + ) + freezer.tick(HASSIO_UPDATE_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert len(issue_registry.issues) == 1 + issue = issue_registry.async_get_issue("homeassistant", issue_id) + assert issue.domain == "homeassistant" + assert issue.severity == ir.IssueSeverity.WARNING + assert issue.translation_placeholders == { + "installation_guide": "https://www.home-assistant.io/installation/", + } + + +@pytest.mark.parametrize( + "arch", + [ + "i386", + "armhf", + "armv7", + ], +) +async def test_deprecated_installation_issue_32bit_method( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, + freezer: FrozenDateTimeFactory, + arch: str, +) -> None: + """Test deprecated architecture issue.""" + with ( + patch.dict(os.environ, MOCK_ENVIRON), + patch( + "homeassistant.components.hassio.async_get_system_info", + return_value={ + "installation_type": "Home Assistant OS", + "arch": arch, + }, + ), + patch( + "homeassistant.components.homeassistant.async_get_system_info", + return_value={ + "installation_type": "Home Assistant OS", + "arch": arch, + }, + ), + patch( + "homeassistant.components.hassio.get_os_info", + return_value={"board": "rpi3-64"}, + ), + patch( + "homeassistant.components.hassio.get_info", return_value={"hassos": True} + ), + patch("homeassistant.components.hardware.async_setup", return_value=True), + ): + assert await async_setup_component(hass, "homeassistant", {}) + config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + freezer.tick(REQUEST_REFRESH_DELAY) + async_fire_time_changed(hass) + await hass.async_block_till_done() + await hass.services.async_call( + "homeassistant", + "update_entity", + { + "entity_id": [ + "update.home_assistant_core_update", + "update.home_assistant_supervisor_update", + ] + }, + blocking=True, + ) + freezer.tick(HASSIO_UPDATE_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert len(issue_registry.issues) == 1 + issue = issue_registry.async_get_issue("homeassistant", "deprecated_architecture") + assert issue.domain == "homeassistant" + assert issue.severity == ir.IssueSeverity.WARNING + assert issue.translation_placeholders == {"installation_type": "OS", "arch": arch} + + +@pytest.mark.parametrize( + "arch", + [ + "i386", + "armhf", + "armv7", + ], +) +async def test_deprecated_installation_issue_32bit_supervised( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, + freezer: FrozenDateTimeFactory, + arch: str, +) -> None: + """Test deprecated architecture issue.""" + with ( + patch.dict(os.environ, MOCK_ENVIRON), + patch( + "homeassistant.components.hassio.async_get_system_info", + return_value={ + "installation_type": "Home Assistant Supervised", + "arch": arch, + }, + ), + patch( + "homeassistant.components.homeassistant.async_get_system_info", + return_value={ + "installation_type": "Home Assistant Supervised", + "arch": arch, + }, + ), + patch( + "homeassistant.components.hassio.get_os_info", + return_value={"board": "rpi3-64"}, + ), + patch( + "homeassistant.components.hassio.get_info", return_value={"hassos": None} + ), + patch("homeassistant.components.hardware.async_setup", return_value=True), + ): + assert await async_setup_component(hass, "homeassistant", {}) + config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + freezer.tick(REQUEST_REFRESH_DELAY) + async_fire_time_changed(hass) + await hass.async_block_till_done() + await hass.services.async_call( + "homeassistant", + "update_entity", + { + "entity_id": [ + "update.home_assistant_core_update", + "update.home_assistant_supervisor_update", + ] + }, + blocking=True, + ) + freezer.tick(HASSIO_UPDATE_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert len(issue_registry.issues) == 1 + issue = issue_registry.async_get_issue( + "homeassistant", "deprecated_method_architecture" + ) + assert issue.domain == "homeassistant" + assert issue.severity == ir.IssueSeverity.WARNING + assert issue.translation_placeholders == { + "installation_type": "Supervised", + "arch": arch, + } + + +@pytest.mark.parametrize( + ("board", "issue_id"), + [ + ("rpi5", "deprecated_os_aarch64"), + ], +) +async def test_deprecated_installation_issue_supported_board( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, + freezer: FrozenDateTimeFactory, + board: str, + issue_id: str, +) -> None: + """Test no deprecated installation issue for a supported board.""" + with ( + patch.dict(os.environ, MOCK_ENVIRON), + patch( + "homeassistant.components.hassio.async_get_system_info", + return_value={ + "installation_type": "Home Assistant OS", + "arch": "aarch64", + }, + ), + patch( + "homeassistant.components.homeassistant.async_get_system_info", + return_value={ + "installation_type": "Home Assistant OS", + "arch": "aarch64", + }, + ), + patch( + "homeassistant.components.hassio.get_os_info", return_value={"board": board} + ), + patch( + "homeassistant.components.hassio.get_info", return_value={"hassos": True} + ), + ): + assert await async_setup_component(hass, "homeassistant", {}) + config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + freezer.tick(REQUEST_REFRESH_DELAY) + async_fire_time_changed(hass) + await hass.async_block_till_done() + await hass.services.async_call( + "homeassistant", + "update_entity", + { + "entity_id": [ + "update.home_assistant_core_update", + "update.home_assistant_supervisor_update", + ] + }, + blocking=True, + ) + freezer.tick(HASSIO_UPDATE_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert len(issue_registry.issues) == 0 diff --git a/tests/components/homeassistant/test_init.py b/tests/components/homeassistant/test_init.py index fe5d2155f58..0010422cd28 100644 --- a/tests/components/homeassistant/test_init.py +++ b/tests/components/homeassistant/test_init.py @@ -640,13 +640,6 @@ async def test_reload_all( assert len(jinja) == 1 -@pytest.mark.parametrize( - "installation_type", - [ - "Home Assistant Core", - "Home Assistant Supervised", - ], -) @pytest.mark.parametrize( "arch", [ @@ -658,14 +651,13 @@ async def test_reload_all( async def test_deprecated_installation_issue_32bit_method( hass: HomeAssistant, issue_registry: ir.IssueRegistry, - installation_type: str, arch: str, ) -> None: """Test deprecated installation issue.""" with patch( "homeassistant.components.homeassistant.async_get_system_info", return_value={ - "installation_type": installation_type, + "installation_type": "Home Assistant Core", "arch": arch, }, ): @@ -679,18 +671,11 @@ async def test_deprecated_installation_issue_32bit_method( assert issue.domain == HOMEASSISTANT_DOMAIN assert issue.severity == ir.IssueSeverity.WARNING assert issue.translation_placeholders == { - "installation_type": installation_type[15:], + "installation_type": "Core", "arch": arch, } -@pytest.mark.parametrize( - "installation_type", - [ - "Home Assistant Container", - "Home Assistant OS", - ], -) @pytest.mark.parametrize( "arch", [ @@ -701,14 +686,13 @@ async def test_deprecated_installation_issue_32bit_method( async def test_deprecated_installation_issue_32bit( hass: HomeAssistant, issue_registry: ir.IssueRegistry, - installation_type: str, arch: str, ) -> None: """Test deprecated installation issue.""" with patch( "homeassistant.components.homeassistant.async_get_system_info", return_value={ - "installation_type": installation_type, + "installation_type": "Home Assistant Container", "arch": arch, }, ): @@ -722,28 +706,19 @@ async def test_deprecated_installation_issue_32bit( assert issue.domain == HOMEASSISTANT_DOMAIN assert issue.severity == ir.IssueSeverity.WARNING assert issue.translation_placeholders == { - "installation_type": installation_type[15:], + "installation_type": "Container", "arch": arch, } -@pytest.mark.parametrize( - "installation_type", - [ - "Home Assistant Core", - "Home Assistant Supervised", - ], -) async def test_deprecated_installation_issue_method( - hass: HomeAssistant, - issue_registry: ir.IssueRegistry, - installation_type: str, + hass: HomeAssistant, issue_registry: ir.IssueRegistry ) -> None: """Test deprecated installation issue.""" with patch( "homeassistant.components.homeassistant.async_get_system_info", return_value={ - "installation_type": installation_type, + "installation_type": "Home Assistant Core", "arch": "generic-x86-64", }, ): @@ -755,52 +730,11 @@ async def test_deprecated_installation_issue_method( assert issue.domain == HOMEASSISTANT_DOMAIN assert issue.severity == ir.IssueSeverity.WARNING assert issue.translation_placeholders == { - "installation_type": installation_type[15:], + "installation_type": "Core", "arch": "generic-x86-64", } -@pytest.mark.parametrize( - ("board", "issue_id"), - [ - ("rpi3", "deprecated_os_aarch64"), - ("rpi4", "deprecated_os_aarch64"), - ("tinker", "deprecated_os_armv7"), - ("odroid-xu4", "deprecated_os_armv7"), - ("rpi2", "deprecated_os_armv7"), - ], -) -async def test_deprecated_installation_issue_aarch64( - hass: HomeAssistant, - issue_registry: ir.IssueRegistry, - board: str, - issue_id: str, -) -> None: - """Test deprecated installation issue.""" - with ( - patch( - "homeassistant.components.homeassistant.async_get_system_info", - return_value={ - "installation_type": "Home Assistant OS", - "arch": "armv7", - }, - ), - patch( - "homeassistant.components.hassio.get_os_info", return_value={"board": board} - ), - ): - assert await async_setup_component(hass, HOMEASSISTANT_DOMAIN, {}) - await hass.async_block_till_done() - - assert len(issue_registry.issues) == 1 - issue = issue_registry.async_get_issue(HOMEASSISTANT_DOMAIN, issue_id) - assert issue.domain == HOMEASSISTANT_DOMAIN - assert issue.severity == ir.IssueSeverity.WARNING - assert issue.translation_placeholders == { - "installation_guide": "https://www.home-assistant.io/installation/", - } - - async def test_deprecated_installation_issue_armv7_container( hass: HomeAssistant, issue_registry: ir.IssueRegistry, From f1df6dcda57b6a7b15003cf9742caffbab1676f6 Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Tue, 10 Jun 2025 22:25:47 +0300 Subject: [PATCH 164/266] Fix Jewish calendar not updating (#146465) --- .../components/jewish_calendar/sensor.py | 9 ++---- .../components/jewish_calendar/test_sensor.py | 31 ++++++++++++++++++- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/jewish_calendar/sensor.py b/homeassistant/components/jewish_calendar/sensor.py index 230adef9894..cb38a3797eb 100644 --- a/homeassistant/components/jewish_calendar/sensor.py +++ b/homeassistant/components/jewish_calendar/sensor.py @@ -225,7 +225,7 @@ async def async_setup_entry( JewishCalendarTimeSensor(config_entry, description) for description in TIME_SENSORS ) - async_add_entities(sensors) + async_add_entities(sensors, update_before_add=True) class JewishCalendarBaseSensor(JewishCalendarEntity, SensorEntity): @@ -233,12 +233,7 @@ class JewishCalendarBaseSensor(JewishCalendarEntity, SensorEntity): _attr_entity_category = EntityCategory.DIAGNOSTIC - async def async_added_to_hass(self) -> None: - """Call when entity is added to hass.""" - await super().async_added_to_hass() - await self.async_update_data() - - async def async_update_data(self) -> None: + async def async_update(self) -> None: """Update the state of the sensor.""" now = dt_util.now() _LOGGER.debug("Now: %s Location: %r", now, self.data.location) diff --git a/tests/components/jewish_calendar/test_sensor.py b/tests/components/jewish_calendar/test_sensor.py index 0cc1e60efc8..38a3dd12206 100644 --- a/tests/components/jewish_calendar/test_sensor.py +++ b/tests/components/jewish_calendar/test_sensor.py @@ -3,6 +3,7 @@ from datetime import datetime as dt from typing import Any +from freezegun.api import FrozenDateTimeFactory from hdate.holidays import HolidayDatabase from hdate.parasha import Parasha import pytest @@ -14,7 +15,7 @@ from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed @pytest.mark.parametrize("language", ["en", "he"]) @@ -542,6 +543,34 @@ async def test_dafyomi_sensor(hass: HomeAssistant, results: str) -> None: assert hass.states.get("sensor.jewish_calendar_daf_yomi").state == results +@pytest.mark.parametrize( + ("test_time", "results"), + [ + ( + dt(2025, 6, 10, 17), + { + "initial_state": "14 Sivan 5785", + "move_to": dt(2025, 6, 10, 23, 0), + "new_state": "15 Sivan 5785", + }, + ), + ], + indirect=True, +) +@pytest.mark.usefixtures("setup_at_time") +async def test_sensor_does_not_update_on_time_change( + hass: HomeAssistant, freezer: FrozenDateTimeFactory, results: dict[str, Any] +) -> None: + """Test that the Jewish calendar sensor does not update after time advances (regression test for update bug).""" + sensor_id = "sensor.jewish_calendar_date" + assert hass.states.get(sensor_id).state == results["initial_state"] + + freezer.move_to(results["move_to"]) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert hass.states.get(sensor_id).state == results["new_state"] + + async def test_no_discovery_info( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: From 69ba2aab11308c66e8a3cf1a895d64eb62be199a Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 10 Jun 2025 20:55:00 +0200 Subject: [PATCH 165/266] Remove DHCP discovery from Amazon Devices (#146476) --- .../components/amazon_devices/manifest.json | 110 ----- .../amazon_devices/quality_scale.yaml | 4 +- homeassistant/generated/dhcp.py | 432 ------------------ .../amazon_devices/test_config_flow.py | 64 +-- 4 files changed, 4 insertions(+), 606 deletions(-) diff --git a/homeassistant/components/amazon_devices/manifest.json b/homeassistant/components/amazon_devices/manifest.json index 37a56486a08..f63893c1598 100644 --- a/homeassistant/components/amazon_devices/manifest.json +++ b/homeassistant/components/amazon_devices/manifest.json @@ -3,116 +3,6 @@ "name": "Amazon Devices", "codeowners": ["@chemelli74"], "config_flow": true, - "dhcp": [ - { "macaddress": "007147*" }, - { "macaddress": "00FC8B*" }, - { "macaddress": "0812A5*" }, - { "macaddress": "086AE5*" }, - { "macaddress": "08849D*" }, - { "macaddress": "089115*" }, - { "macaddress": "08A6BC*" }, - { "macaddress": "08C224*" }, - { "macaddress": "0CDC91*" }, - { "macaddress": "0CEE99*" }, - { "macaddress": "1009F9*" }, - { "macaddress": "109693*" }, - { "macaddress": "10BF67*" }, - { "macaddress": "10CE02*" }, - { "macaddress": "140AC5*" }, - { "macaddress": "149138*" }, - { "macaddress": "1848BE*" }, - { "macaddress": "1C12B0*" }, - { "macaddress": "1C4D66*" }, - { "macaddress": "1C93C4*" }, - { "macaddress": "1CFE2B*" }, - { "macaddress": "244CE3*" }, - { "macaddress": "24CE33*" }, - { "macaddress": "2873F6*" }, - { "macaddress": "2C71FF*" }, - { "macaddress": "34AFB3*" }, - { "macaddress": "34D270*" }, - { "macaddress": "38F73D*" }, - { "macaddress": "3C5CC4*" }, - { "macaddress": "3CE441*" }, - { "macaddress": "440049*" }, - { "macaddress": "40A2DB*" }, - { "macaddress": "40A9CF*" }, - { "macaddress": "40B4CD*" }, - { "macaddress": "443D54*" }, - { "macaddress": "44650D*" }, - { "macaddress": "485F2D*" }, - { "macaddress": "48785E*" }, - { "macaddress": "48B423*" }, - { "macaddress": "4C1744*" }, - { "macaddress": "4CEFC0*" }, - { "macaddress": "5007C3*" }, - { "macaddress": "50D45C*" }, - { "macaddress": "50DCE7*" }, - { "macaddress": "50F5DA*" }, - { "macaddress": "5C415A*" }, - { "macaddress": "6837E9*" }, - { "macaddress": "6854FD*" }, - { "macaddress": "689A87*" }, - { "macaddress": "68B691*" }, - { "macaddress": "68DBF5*" }, - { "macaddress": "68F63B*" }, - { "macaddress": "6C0C9A*" }, - { "macaddress": "6C5697*" }, - { "macaddress": "7458F3*" }, - { "macaddress": "74C246*" }, - { "macaddress": "74D637*" }, - { "macaddress": "74E20C*" }, - { "macaddress": "74ECB2*" }, - { "macaddress": "786C84*" }, - { "macaddress": "78A03F*" }, - { "macaddress": "7C6166*" }, - { "macaddress": "7C6305*" }, - { "macaddress": "7CD566*" }, - { "macaddress": "8871E5*" }, - { "macaddress": "901195*" }, - { "macaddress": "90235B*" }, - { "macaddress": "90A822*" }, - { "macaddress": "90F82E*" }, - { "macaddress": "943A91*" }, - { "macaddress": "98226E*" }, - { "macaddress": "98CCF3*" }, - { "macaddress": "9CC8E9*" }, - { "macaddress": "A002DC*" }, - { "macaddress": "A0D2B1*" }, - { "macaddress": "A40801*" }, - { "macaddress": "A8E621*" }, - { "macaddress": "AC416A*" }, - { "macaddress": "AC63BE*" }, - { "macaddress": "ACCCFC*" }, - { "macaddress": "B0739C*" }, - { "macaddress": "B0CFCB*" }, - { "macaddress": "B0F7C4*" }, - { "macaddress": "B85F98*" }, - { "macaddress": "C091B9*" }, - { "macaddress": "C095CF*" }, - { "macaddress": "C49500*" }, - { "macaddress": "C86C3D*" }, - { "macaddress": "CC9EA2*" }, - { "macaddress": "CCF735*" }, - { "macaddress": "DC54D7*" }, - { "macaddress": "D8BE65*" }, - { "macaddress": "D8FBD6*" }, - { "macaddress": "DC91BF*" }, - { "macaddress": "DCA0D0*" }, - { "macaddress": "E0F728*" }, - { "macaddress": "EC2BEB*" }, - { "macaddress": "EC8AC4*" }, - { "macaddress": "ECA138*" }, - { "macaddress": "F02F9E*" }, - { "macaddress": "F0272D*" }, - { "macaddress": "F0F0A4*" }, - { "macaddress": "F4032A*" }, - { "macaddress": "F854B8*" }, - { "macaddress": "FC492D*" }, - { "macaddress": "FC65DE*" }, - { "macaddress": "FCA183*" }, - { "macaddress": "FCE9D8*" } - ], "documentation": "https://www.home-assistant.io/integrations/amazon_devices", "integration_type": "hub", "iot_class": "cloud_polling", diff --git a/homeassistant/components/amazon_devices/quality_scale.yaml b/homeassistant/components/amazon_devices/quality_scale.yaml index 23a7cd22a66..881a02bc6d3 100644 --- a/homeassistant/components/amazon_devices/quality_scale.yaml +++ b/homeassistant/components/amazon_devices/quality_scale.yaml @@ -45,7 +45,9 @@ rules: discovery-update-info: status: exempt comment: Network information not relevant - discovery: done + discovery: + status: exempt + 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: todo docs-examples: todo docs-known-limitations: todo diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 5285ab7a1db..349c69358ba 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -26,438 +26,6 @@ DHCP: Final[list[dict[str, str | bool]]] = [ "domain": "airzone", "macaddress": "E84F25*", }, - { - "domain": "amazon_devices", - "macaddress": "007147*", - }, - { - "domain": "amazon_devices", - "macaddress": "00FC8B*", - }, - { - "domain": "amazon_devices", - "macaddress": "0812A5*", - }, - { - "domain": "amazon_devices", - "macaddress": "086AE5*", - }, - { - "domain": "amazon_devices", - "macaddress": "08849D*", - }, - { - "domain": "amazon_devices", - "macaddress": "089115*", - }, - { - "domain": "amazon_devices", - "macaddress": "08A6BC*", - }, - { - "domain": "amazon_devices", - "macaddress": "08C224*", - }, - { - "domain": "amazon_devices", - "macaddress": "0CDC91*", - }, - { - "domain": "amazon_devices", - "macaddress": "0CEE99*", - }, - { - "domain": "amazon_devices", - "macaddress": "1009F9*", - }, - { - "domain": "amazon_devices", - "macaddress": "109693*", - }, - { - "domain": "amazon_devices", - "macaddress": "10BF67*", - }, - { - "domain": "amazon_devices", - "macaddress": "10CE02*", - }, - { - "domain": "amazon_devices", - "macaddress": "140AC5*", - }, - { - "domain": "amazon_devices", - "macaddress": "149138*", - }, - { - "domain": "amazon_devices", - "macaddress": "1848BE*", - }, - { - "domain": "amazon_devices", - "macaddress": "1C12B0*", - }, - { - "domain": "amazon_devices", - "macaddress": "1C4D66*", - }, - { - "domain": "amazon_devices", - "macaddress": "1C93C4*", - }, - { - "domain": "amazon_devices", - "macaddress": "1CFE2B*", - }, - { - "domain": "amazon_devices", - "macaddress": "244CE3*", - }, - { - "domain": "amazon_devices", - "macaddress": "24CE33*", - }, - { - "domain": "amazon_devices", - "macaddress": "2873F6*", - }, - { - "domain": "amazon_devices", - "macaddress": "2C71FF*", - }, - { - "domain": "amazon_devices", - "macaddress": "34AFB3*", - }, - { - "domain": "amazon_devices", - "macaddress": "34D270*", - }, - { - "domain": "amazon_devices", - "macaddress": "38F73D*", - }, - { - "domain": "amazon_devices", - "macaddress": "3C5CC4*", - }, - { - "domain": "amazon_devices", - "macaddress": "3CE441*", - }, - { - "domain": "amazon_devices", - "macaddress": "440049*", - }, - { - "domain": "amazon_devices", - "macaddress": "40A2DB*", - }, - { - "domain": "amazon_devices", - "macaddress": "40A9CF*", - }, - { - "domain": "amazon_devices", - "macaddress": "40B4CD*", - }, - { - "domain": "amazon_devices", - "macaddress": "443D54*", - }, - { - "domain": "amazon_devices", - "macaddress": "44650D*", - }, - { - "domain": "amazon_devices", - "macaddress": "485F2D*", - }, - { - "domain": "amazon_devices", - "macaddress": "48785E*", - }, - { - "domain": "amazon_devices", - "macaddress": "48B423*", - }, - { - "domain": "amazon_devices", - "macaddress": "4C1744*", - }, - { - "domain": "amazon_devices", - "macaddress": "4CEFC0*", - }, - { - "domain": "amazon_devices", - "macaddress": "5007C3*", - }, - { - "domain": "amazon_devices", - "macaddress": "50D45C*", - }, - { - "domain": "amazon_devices", - "macaddress": "50DCE7*", - }, - { - "domain": "amazon_devices", - "macaddress": "50F5DA*", - }, - { - "domain": "amazon_devices", - "macaddress": "5C415A*", - }, - { - "domain": "amazon_devices", - "macaddress": "6837E9*", - }, - { - "domain": "amazon_devices", - "macaddress": "6854FD*", - }, - { - "domain": "amazon_devices", - "macaddress": "689A87*", - }, - { - "domain": "amazon_devices", - "macaddress": "68B691*", - }, - { - "domain": "amazon_devices", - "macaddress": "68DBF5*", - }, - { - "domain": "amazon_devices", - "macaddress": "68F63B*", - }, - { - "domain": "amazon_devices", - "macaddress": "6C0C9A*", - }, - { - "domain": "amazon_devices", - "macaddress": "6C5697*", - }, - { - "domain": "amazon_devices", - "macaddress": "7458F3*", - }, - { - "domain": "amazon_devices", - "macaddress": "74C246*", - }, - { - "domain": "amazon_devices", - "macaddress": "74D637*", - }, - { - "domain": "amazon_devices", - "macaddress": "74E20C*", - }, - { - "domain": "amazon_devices", - "macaddress": "74ECB2*", - }, - { - "domain": "amazon_devices", - "macaddress": "786C84*", - }, - { - "domain": "amazon_devices", - "macaddress": "78A03F*", - }, - { - "domain": "amazon_devices", - "macaddress": "7C6166*", - }, - { - "domain": "amazon_devices", - "macaddress": "7C6305*", - }, - { - "domain": "amazon_devices", - "macaddress": "7CD566*", - }, - { - "domain": "amazon_devices", - "macaddress": "8871E5*", - }, - { - "domain": "amazon_devices", - "macaddress": "901195*", - }, - { - "domain": "amazon_devices", - "macaddress": "90235B*", - }, - { - "domain": "amazon_devices", - "macaddress": "90A822*", - }, - { - "domain": "amazon_devices", - "macaddress": "90F82E*", - }, - { - "domain": "amazon_devices", - "macaddress": "943A91*", - }, - { - "domain": "amazon_devices", - "macaddress": "98226E*", - }, - { - "domain": "amazon_devices", - "macaddress": "98CCF3*", - }, - { - "domain": "amazon_devices", - "macaddress": "9CC8E9*", - }, - { - "domain": "amazon_devices", - "macaddress": "A002DC*", - }, - { - "domain": "amazon_devices", - "macaddress": "A0D2B1*", - }, - { - "domain": "amazon_devices", - "macaddress": "A40801*", - }, - { - "domain": "amazon_devices", - "macaddress": "A8E621*", - }, - { - "domain": "amazon_devices", - "macaddress": "AC416A*", - }, - { - "domain": "amazon_devices", - "macaddress": "AC63BE*", - }, - { - "domain": "amazon_devices", - "macaddress": "ACCCFC*", - }, - { - "domain": "amazon_devices", - "macaddress": "B0739C*", - }, - { - "domain": "amazon_devices", - "macaddress": "B0CFCB*", - }, - { - "domain": "amazon_devices", - "macaddress": "B0F7C4*", - }, - { - "domain": "amazon_devices", - "macaddress": "B85F98*", - }, - { - "domain": "amazon_devices", - "macaddress": "C091B9*", - }, - { - "domain": "amazon_devices", - "macaddress": "C095CF*", - }, - { - "domain": "amazon_devices", - "macaddress": "C49500*", - }, - { - "domain": "amazon_devices", - "macaddress": "C86C3D*", - }, - { - "domain": "amazon_devices", - "macaddress": "CC9EA2*", - }, - { - "domain": "amazon_devices", - "macaddress": "CCF735*", - }, - { - "domain": "amazon_devices", - "macaddress": "DC54D7*", - }, - { - "domain": "amazon_devices", - "macaddress": "D8BE65*", - }, - { - "domain": "amazon_devices", - "macaddress": "D8FBD6*", - }, - { - "domain": "amazon_devices", - "macaddress": "DC91BF*", - }, - { - "domain": "amazon_devices", - "macaddress": "DCA0D0*", - }, - { - "domain": "amazon_devices", - "macaddress": "E0F728*", - }, - { - "domain": "amazon_devices", - "macaddress": "EC2BEB*", - }, - { - "domain": "amazon_devices", - "macaddress": "EC8AC4*", - }, - { - "domain": "amazon_devices", - "macaddress": "ECA138*", - }, - { - "domain": "amazon_devices", - "macaddress": "F02F9E*", - }, - { - "domain": "amazon_devices", - "macaddress": "F0272D*", - }, - { - "domain": "amazon_devices", - "macaddress": "F0F0A4*", - }, - { - "domain": "amazon_devices", - "macaddress": "F4032A*", - }, - { - "domain": "amazon_devices", - "macaddress": "F854B8*", - }, - { - "domain": "amazon_devices", - "macaddress": "FC492D*", - }, - { - "domain": "amazon_devices", - "macaddress": "FC65DE*", - }, - { - "domain": "amazon_devices", - "macaddress": "FCA183*", - }, - { - "domain": "amazon_devices", - "macaddress": "FCE9D8*", - }, { "domain": "august", "hostname": "connect", diff --git a/tests/components/amazon_devices/test_config_flow.py b/tests/components/amazon_devices/test_config_flow.py index 41b65c33bd5..ce1ac44d102 100644 --- a/tests/components/amazon_devices/test_config_flow.py +++ b/tests/components/amazon_devices/test_config_flow.py @@ -6,22 +6,15 @@ from aioamazondevices.exceptions import CannotAuthenticate, CannotConnect import pytest from homeassistant.components.amazon_devices.const import CONF_LOGIN_DATA, DOMAIN -from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER +from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_CODE, CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from .const import TEST_CODE, TEST_COUNTRY, TEST_PASSWORD, TEST_USERNAME from tests.common import MockConfigEntry -DHCP_DISCOVERY = DhcpServiceInfo( - ip="1.1.1.1", - hostname="", - macaddress="c095cfebf19f", -) - async def test_full_flow( hass: HomeAssistant, @@ -140,58 +133,3 @@ async def test_already_configured( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" - - -async def test_dhcp_flow( - hass: HomeAssistant, - mock_amazon_devices_client: AsyncMock, - mock_setup_entry: AsyncMock, -) -> None: - """Test full DHCP flow.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_DHCP}, - data=DHCP_DISCOVERY, - ) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "user" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_COUNTRY: TEST_COUNTRY, - CONF_USERNAME: TEST_USERNAME, - CONF_PASSWORD: TEST_PASSWORD, - CONF_CODE: TEST_CODE, - }, - ) - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == TEST_USERNAME - assert result["data"] == { - CONF_COUNTRY: TEST_COUNTRY, - CONF_USERNAME: TEST_USERNAME, - CONF_PASSWORD: TEST_PASSWORD, - CONF_LOGIN_DATA: { - "customer_info": {"user_id": TEST_USERNAME}, - }, - } - assert result["result"].unique_id == TEST_USERNAME - - -async def test_dhcp_already_configured( - hass: HomeAssistant, - mock_amazon_devices_client: AsyncMock, - mock_setup_entry: AsyncMock, - mock_config_entry: MockConfigEntry, -) -> None: - """Test duplicate flow.""" - mock_config_entry.add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_DHCP}, - data=DHCP_DISCOVERY, - ) - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "already_configured" From 8fd52248b78e9be47ce910455829d29cd4a3a321 Mon Sep 17 00:00:00 2001 From: Felix Schneider Date: Wed, 11 Jun 2025 10:26:01 +0200 Subject: [PATCH 166/266] Bump `apsystems` to `2.7.0` (#146485) --- homeassistant/components/apsystems/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/apsystems/manifest.json b/homeassistant/components/apsystems/manifest.json index eb1acb40d17..e86b4a8431e 100644 --- a/homeassistant/components/apsystems/manifest.json +++ b/homeassistant/components/apsystems/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["APsystemsEZ1"], - "requirements": ["apsystems-ez1==2.6.0"] + "requirements": ["apsystems-ez1==2.7.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 98b43cb4635..1409a24ab25 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -498,7 +498,7 @@ apprise==1.9.1 aprslib==0.7.2 # homeassistant.components.apsystems -apsystems-ez1==2.6.0 +apsystems-ez1==2.7.0 # homeassistant.components.aqualogic aqualogic==2.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 23ace366641..7a283276028 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -471,7 +471,7 @@ apprise==1.9.1 aprslib==0.7.2 # homeassistant.components.apsystems -apsystems-ez1==2.6.0 +apsystems-ez1==2.7.0 # homeassistant.components.aranet aranet4==2.5.1 From 7afc469306f6298a3d8ffc0e9847db69dc6be0b8 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Tue, 10 Jun 2025 18:16:18 -0500 Subject: [PATCH 167/266] Bump intents to 2025.6.10 (#146491) --- homeassistant/components/conversation/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/hassfest/docker/Dockerfile | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 6078d73e99b..5221e89deee 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.5.28"] + "requirements": ["hassil==2.2.3", "home-assistant-intents==2025.6.10"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index af9b0472bb1..b5d1af412f9 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -39,7 +39,7 @@ hass-nabucasa==0.101.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 home-assistant-frontend==20250531.0 -home-assistant-intents==2025.5.28 +home-assistant-intents==2025.6.10 httpx==0.28.1 ifaddr==0.2.0 Jinja2==3.1.6 diff --git a/pyproject.toml b/pyproject.toml index 58ad46b63e3..b096fcb8040 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,7 +66,7 @@ dependencies = [ # onboarding->cloud->assist_pipeline->conversation->home_assistant_intents. Onboarding needs # to be setup in stage 0, but we don't want to also promote cloud with all its # dependencies to stage 0. - "home-assistant-intents==2025.5.28", + "home-assistant-intents==2025.6.10", "ifaddr==0.2.0", "Jinja2==3.1.6", "lru-dict==1.3.0", diff --git a/requirements.txt b/requirements.txt index bff95490470..e353adac9d3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -27,7 +27,7 @@ hass-nabucasa==0.101.0 hassil==2.2.3 httpx==0.28.1 home-assistant-bluetooth==1.13.1 -home-assistant-intents==2025.5.28 +home-assistant-intents==2025.6.10 ifaddr==0.2.0 Jinja2==3.1.6 lru-dict==1.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index 1409a24ab25..967a6defd14 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1167,7 +1167,7 @@ holidays==0.74 home-assistant-frontend==20250531.0 # homeassistant.components.conversation -home-assistant-intents==2025.5.28 +home-assistant-intents==2025.6.10 # homeassistant.components.homematicip_cloud homematicip==2.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7a283276028..6632e45ed6e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1013,7 +1013,7 @@ holidays==0.74 home-assistant-frontend==20250531.0 # homeassistant.components.conversation -home-assistant-intents==2025.5.28 +home-assistant-intents==2025.6.10 # homeassistant.components.homematicip_cloud homematicip==2.0.4 diff --git a/script/hassfest/docker/Dockerfile b/script/hassfest/docker/Dockerfile index 830bdc4445e..82150d031a4 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.5.28 \ + home-assistant-intents==2025.6.10 \ mutagen==1.47.0 \ pymicro-vad==1.0.1 \ pyspeex-noise==1.0.2 From 671a33b31c88817bea2c21f05d24ddd702a403e7 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 11 Jun 2025 11:19:44 +0200 Subject: [PATCH 168/266] Do not remove derivative config entry when input sensor is removed (#146506) * Do not remove derivative config entry when input sensor is removed * Add comments * Update homeassistant/helpers/helper_integration.py Co-authored-by: Martin Hjelmare --------- Co-authored-by: Franck Nijhof Co-authored-by: Martin Hjelmare --- .../components/derivative/__init__.py | 5 ++++ .../components/switch_as_x/__init__.py | 6 ++++ homeassistant/helpers/device.py | 6 ++-- homeassistant/helpers/helper_integration.py | 14 +++++++-- tests/components/derivative/test_init.py | 8 ++--- tests/helpers/test_helper_integration.py | 30 ++++++++++++------- 6 files changed, 51 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/derivative/__init__.py b/homeassistant/components/derivative/__init__.py index 5eb499b0efd..6d539817875 100644 --- a/homeassistant/components/derivative/__init__.py +++ b/homeassistant/components/derivative/__init__.py @@ -29,6 +29,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: options={**entry.options, CONF_SOURCE: source_entity_id}, ) + async def source_entity_removed() -> None: + # The source entity has been removed, we need to clean the device links. + async_remove_stale_devices_links_keep_entity_device(hass, entry.entry_id, None) + entity_registry = er.async_get(hass) entry.async_on_unload( async_handle_source_entity_changes( @@ -42,6 +46,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, entry.options[CONF_SOURCE] ), source_entity_id_or_uuid=entry.options[CONF_SOURCE], + source_entity_removed=source_entity_removed, ) ) await hass.config_entries.async_forward_entry_setups(entry, (Platform.SENSOR,)) diff --git a/homeassistant/components/switch_as_x/__init__.py b/homeassistant/components/switch_as_x/__init__.py index 6e9e3a93b45..7d12ae4aec2 100644 --- a/homeassistant/components/switch_as_x/__init__.py +++ b/homeassistant/components/switch_as_x/__init__.py @@ -63,6 +63,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: options={**entry.options, CONF_ENTITY_ID: source_entity_id}, ) + async def source_entity_removed() -> None: + # The source entity has been removed, we remove the config entry because + # switch_as_x does not allow replacing the wrapped entity. + await hass.config_entries.async_remove(entry.entry_id) + entry.async_on_unload( async_handle_source_entity_changes( hass, @@ -73,6 +78,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: set_source_entity_id_or_uuid=set_source_entity_id_or_uuid, source_device_id=async_add_to_device(hass, entry, entity_id), source_entity_id_or_uuid=entry.options[CONF_ENTITY_ID], + source_entity_removed=source_entity_removed, ) ) entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) diff --git a/homeassistant/helpers/device.py b/homeassistant/helpers/device.py index a7d888900b1..f1404bb068b 100644 --- a/homeassistant/helpers/device.py +++ b/homeassistant/helpers/device.py @@ -62,7 +62,7 @@ def async_device_info_to_link_from_device_id( def async_remove_stale_devices_links_keep_entity_device( hass: HomeAssistant, entry_id: str, - source_entity_id_or_uuid: str, + source_entity_id_or_uuid: str | None, ) -> None: """Remove entry_id from all devices except that of source_entity_id_or_uuid. @@ -73,7 +73,9 @@ def async_remove_stale_devices_links_keep_entity_device( async_remove_stale_devices_links_keep_current_device( hass=hass, entry_id=entry_id, - current_device_id=async_entity_id_to_device_id(hass, source_entity_id_or_uuid), + current_device_id=async_entity_id_to_device_id(hass, source_entity_id_or_uuid) + if source_entity_id_or_uuid + else None, ) diff --git a/homeassistant/helpers/helper_integration.py b/homeassistant/helpers/helper_integration.py index 4f39ef4c843..37aa246178e 100644 --- a/homeassistant/helpers/helper_integration.py +++ b/homeassistant/helpers/helper_integration.py @@ -2,7 +2,8 @@ from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Coroutine +from typing import Any from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, valid_entity_id @@ -18,6 +19,7 @@ def async_handle_source_entity_changes( set_source_entity_id_or_uuid: Callable[[str], None], source_device_id: str | None, source_entity_id_or_uuid: str, + source_entity_removed: Callable[[], Coroutine[Any, Any, None]], ) -> CALLBACK_TYPE: """Handle changes to a helper entity's source entity. @@ -34,6 +36,14 @@ def async_handle_source_entity_changes( - Source entity removed from the device: The helper entity is updated to link to no device, and the helper config entry removed from the old device. Then the helper config entry is reloaded. + + :param get_helper_entity: A function which returns the helper entity's entity ID, + or None if the helper entity does not exist. + :param set_source_entity_id_or_uuid: A function which updates the source entity + ID or UUID, e.g., in the helper config entry options. + :param source_entity_removed: A function which is called when the source entity + is removed. This can be used to clean up any resources related to the source + entity or ask the user to select a new source entity. """ async def async_registry_updated( @@ -44,7 +54,7 @@ def async_handle_source_entity_changes( data = event.data if data["action"] == "remove": - await hass.config_entries.async_remove(helper_config_entry_id) + await source_entity_removed() if data["action"] != "update": return diff --git a/tests/components/derivative/test_init.py b/tests/components/derivative/test_init.py index f75d5940da7..d237703eb2e 100644 --- a/tests/components/derivative/test_init.py +++ b/tests/components/derivative/test_init.py @@ -268,17 +268,17 @@ async def test_async_handle_source_entity_changes_source_entity_removed( ) await hass.async_block_till_done() await hass.async_block_till_done() - mock_unload_entry.assert_called_once() + mock_unload_entry.assert_not_called() # Check that the derivative config entry is removed from the device sensor_device = device_registry.async_get(sensor_device.id) assert derivative_config_entry.entry_id not in sensor_device.config_entries - # Check that the derivative config entry is removed - assert derivative_config_entry.entry_id not in hass.config_entries.async_entry_ids() + # Check that the derivative config entry is not removed + assert derivative_config_entry.entry_id in hass.config_entries.async_entry_ids() # Check we got the expected events - assert events == ["remove"] + assert events == ["update"] async def test_async_handle_source_entity_changes_source_entity_removed_from_device( diff --git a/tests/helpers/test_helper_integration.py b/tests/helpers/test_helper_integration.py index 25d490c27bb..12433894dc7 100644 --- a/tests/helpers/test_helper_integration.py +++ b/tests/helpers/test_helper_integration.py @@ -132,11 +132,17 @@ def async_unload_entry() -> AsyncMock: @pytest.fixture -def set_source_entity_id_or_uuid() -> AsyncMock: - """Fixture to mock async_unload_entry.""" +def set_source_entity_id_or_uuid() -> Mock: + """Fixture to mock set_source_entity_id_or_uuid.""" return Mock() +@pytest.fixture +def source_entity_removed() -> AsyncMock: + """Fixture to mock source_entity_removed.""" + return AsyncMock() + + @pytest.fixture def mock_helper_integration( hass: HomeAssistant, @@ -146,6 +152,7 @@ def mock_helper_integration( async_remove_entry: AsyncMock, async_unload_entry: AsyncMock, set_source_entity_id_or_uuid: Mock, + source_entity_removed: AsyncMock, ) -> None: """Mock the helper integration.""" @@ -164,6 +171,7 @@ def mock_helper_integration( set_source_entity_id_or_uuid=set_source_entity_id_or_uuid, source_device_id=source_entity_entry.device_id, source_entity_id_or_uuid=helper_config_entry.options["source"], + source_entity_removed=source_entity_removed, ) return True @@ -206,6 +214,7 @@ async def test_async_handle_source_entity_changes_source_entity_removed( async_remove_entry: AsyncMock, async_unload_entry: AsyncMock, set_source_entity_id_or_uuid: Mock, + source_entity_removed: AsyncMock, ) -> None: """Test the helper config entry is removed when the source entity is removed.""" # Add the helper config entry to the source device @@ -238,20 +247,21 @@ async def test_async_handle_source_entity_changes_source_entity_removed( await hass.async_block_till_done() await hass.async_block_till_done() - # Check that the helper config entry is unloaded and removed - async_unload_entry.assert_called_once() - async_remove_entry.assert_called_once() + # Check that the source_entity_removed callback was called + source_entity_removed.assert_called_once() + async_unload_entry.assert_not_called() + async_remove_entry.assert_not_called() set_source_entity_id_or_uuid.assert_not_called() - # Check that the helper config entry is removed from the device + # Check that the helper config entry is not removed from the device source_device = device_registry.async_get(source_device.id) - assert helper_config_entry.entry_id not in source_device.config_entries + assert helper_config_entry.entry_id in source_device.config_entries - # Check that the helper config entry is removed - assert helper_config_entry.entry_id not in hass.config_entries.async_entry_ids() + # Check that the helper config entry is not removed + assert helper_config_entry.entry_id in hass.config_entries.async_entry_ids() # Check we got the expected events - assert events == ["remove"] + assert events == [] @pytest.mark.parametrize("use_entity_registry_id", [True, False]) From 6d1f621e550311eb915ba31dbd9f1037a7b9519f Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 11 Jun 2025 09:49:38 +0100 Subject: [PATCH 169/266] Bump deebot-client to 13.3.0 (#146507) --- homeassistant/components/ecovacs/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ecovacs/manifest.json b/homeassistant/components/ecovacs/manifest.json index 12fd8e01215..8a7388da735 100644 --- a/homeassistant/components/ecovacs/manifest.json +++ b/homeassistant/components/ecovacs/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/ecovacs", "iot_class": "cloud_push", "loggers": ["sleekxmppfs", "sucks", "deebot_client"], - "requirements": ["py-sucks==0.9.11", "deebot-client==13.2.1"] + "requirements": ["py-sucks==0.9.11", "deebot-client==13.3.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 967a6defd14..95a5f919add 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -765,7 +765,7 @@ debugpy==1.8.14 # decora==0.6 # homeassistant.components.ecovacs -deebot-client==13.2.1 +deebot-client==13.3.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6632e45ed6e..1ed51e54164 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -665,7 +665,7 @@ debugpy==1.8.14 # decora==0.6 # homeassistant.components.ecovacs -deebot-client==13.2.1 +deebot-client==13.3.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns From c8b70cc0fb72ce52e60c01b60f2b252005f642b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Wed, 11 Jun 2025 11:55:28 +0200 Subject: [PATCH 170/266] Graceful handling of missing datapoint in myuplink (#146517) --- homeassistant/components/myuplink/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/myuplink/sensor.py b/homeassistant/components/myuplink/sensor.py index 3b14cdd4630..0a3f7d2ebb6 100644 --- a/homeassistant/components/myuplink/sensor.py +++ b/homeassistant/components/myuplink/sensor.py @@ -293,8 +293,8 @@ class MyUplinkDevicePointSensor(MyUplinkEntity, SensorEntity): @property def native_value(self) -> StateType: """Sensor state value.""" - device_point = self.coordinator.data.points[self.device_id][self.point_id] - if device_point.value == MARKER_FOR_UNKNOWN_VALUE: + device_point = self.coordinator.data.points[self.device_id].get(self.point_id) + if device_point is None or device_point.value == MARKER_FOR_UNKNOWN_VALUE: return None return device_point.value # type: ignore[no-any-return] From b6c8718ae43c186615095124783528b98ff884de Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 11 Jun 2025 10:17:18 +0000 Subject: [PATCH 171/266] Bump version to 2025.6.0b7 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index b07549d387f..6cbe42d6dbe 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 = 6 -PATCH_VERSION: Final = "0b6" +PATCH_VERSION: Final = "0b7" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index b096fcb8040..f0aa8169054 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.6.0b6" +version = "2025.6.0b7" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From 66be2f924020842225fb44ad0d66a16a1b3be09f Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Wed, 11 Jun 2025 09:08:10 -0400 Subject: [PATCH 172/266] Fix `delay_on` and `delay_off` restarting when a new trigger occurs during the delay (#145050) --- .../components/template/binary_sensor.py | 24 ++++++-- .../components/template/test_binary_sensor.py | 56 +++++++++++++++++++ 2 files changed, 74 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index 7ef64e8077b..f0ec64eae2a 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -352,6 +352,8 @@ class TriggerBinarySensorEntity(TriggerEntity, BinarySensorEntity, RestoreEntity self._to_render_simple.append(key) self._parse_result.add(key) + self._last_delay_from: bool | None = None + self._last_delay_to: bool | None = None self._delay_cancel: CALLBACK_TYPE | None = None self._auto_off_cancel: CALLBACK_TYPE | None = None self._auto_off_time: datetime | None = None @@ -388,6 +390,20 @@ class TriggerBinarySensorEntity(TriggerEntity, BinarySensorEntity, RestoreEntity """Handle update of the data.""" self._process_data() + raw = self._rendered.get(CONF_STATE) + state = template.result_as_boolean(raw) + + key = CONF_DELAY_ON if state else CONF_DELAY_OFF + delay = self._rendered.get(key) or self._config.get(key) + + if ( + self._delay_cancel + and delay + and self._attr_is_on == self._last_delay_from + and state == self._last_delay_to + ): + return + if self._delay_cancel: self._delay_cancel() self._delay_cancel = None @@ -401,12 +417,6 @@ class TriggerBinarySensorEntity(TriggerEntity, BinarySensorEntity, RestoreEntity self.async_write_ha_state() return - raw = self._rendered.get(CONF_STATE) - state = template.result_as_boolean(raw) - - key = CONF_DELAY_ON if state else CONF_DELAY_OFF - delay = self._rendered.get(key) or self._config.get(key) - # state without delay. None means rendering failed. if self._attr_is_on == state or state is None or delay is None: self._set_state(state) @@ -422,6 +432,8 @@ class TriggerBinarySensorEntity(TriggerEntity, BinarySensorEntity, RestoreEntity return # state with delay. Cancelled if new trigger received + self._last_delay_from = self._attr_is_on + self._last_delay_to = state self._delay_cancel = async_call_later( self.hass, delay.total_seconds(), partial(self._set_state, state) ) diff --git a/tests/components/template/test_binary_sensor.py b/tests/components/template/test_binary_sensor.py index a7ee953bb09..122801e6c59 100644 --- a/tests/components/template/test_binary_sensor.py +++ b/tests/components/template/test_binary_sensor.py @@ -1225,6 +1225,62 @@ async def test_template_with_trigger_templated_delay_on(hass: HomeAssistant) -> assert state.state == STATE_OFF +@pytest.mark.parametrize(("count", "domain"), [(1, "template")]) +@pytest.mark.parametrize( + ("config", "delay_state"), + [ + ( + { + "template": { + "trigger": {"platform": "event", "event_type": "test_event"}, + "binary_sensor": { + "name": "test", + "state": "{{ trigger.event.data.beer == 2 }}", + "device_class": "motion", + "delay_on": '{{ ({ "seconds": 10 }) }}', + }, + }, + }, + STATE_ON, + ), + ( + { + "template": { + "trigger": {"platform": "event", "event_type": "test_event"}, + "binary_sensor": { + "name": "test", + "state": "{{ trigger.event.data.beer != 2 }}", + "device_class": "motion", + "delay_off": '{{ ({ "seconds": 10 }) }}', + }, + }, + }, + STATE_OFF, + ), + ], +) +@pytest.mark.usefixtures("start_ha") +async def test_trigger_template_delay_with_multiple_triggers( + hass: HomeAssistant, delay_state: str +) -> None: + """Test trigger based binary sensor with multiple triggers occurring during the delay.""" + future = dt_util.utcnow() + for _ in range(10): + # State should still be unknown + state = hass.states.get("binary_sensor.test") + assert state.state == STATE_UNKNOWN + + hass.bus.async_fire("test_event", {"beer": 2}, context=Context()) + await hass.async_block_till_done() + + future += timedelta(seconds=1) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == delay_state + + @pytest.mark.parametrize(("count", "domain"), [(1, "template")]) @pytest.mark.parametrize( "config", From 6f4e16eed1a0c90657f107a793d40d42e6bb87b9 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Wed, 11 Jun 2025 18:17:11 +0200 Subject: [PATCH 173/266] Fix stale options in here_travel_time (#145911) --- .../components/here_travel_time/__init__.py | 33 +--- .../here_travel_time/coordinator.py | 144 +++++++++++------- .../components/here_travel_time/model.py | 18 +-- 3 files changed, 94 insertions(+), 101 deletions(-) diff --git a/homeassistant/components/here_travel_time/__init__.py b/homeassistant/components/here_travel_time/__init__.py index 525da15bd74..5393dfa5050 100644 --- a/homeassistant/components/here_travel_time/__init__.py +++ b/homeassistant/components/here_travel_time/__init__.py @@ -5,26 +5,13 @@ from __future__ import annotations from homeassistant.const import CONF_API_KEY, CONF_MODE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.start import async_at_started -from homeassistant.util import dt as dt_util -from .const import ( - CONF_ARRIVAL_TIME, - CONF_DEPARTURE_TIME, - CONF_DESTINATION_ENTITY_ID, - CONF_DESTINATION_LATITUDE, - CONF_DESTINATION_LONGITUDE, - CONF_ORIGIN_ENTITY_ID, - CONF_ORIGIN_LATITUDE, - CONF_ORIGIN_LONGITUDE, - CONF_ROUTE_MODE, - TRAVEL_MODE_PUBLIC, -) +from .const import TRAVEL_MODE_PUBLIC from .coordinator import ( HereConfigEntry, HERERoutingDataUpdateCoordinator, HERETransitDataUpdateCoordinator, ) -from .model import HERETravelTimeConfig PLATFORMS = [Platform.SENSOR] @@ -33,29 +20,13 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: HereConfigEntry) """Set up HERE Travel Time from a config entry.""" api_key = config_entry.data[CONF_API_KEY] - arrival = dt_util.parse_time(config_entry.options.get(CONF_ARRIVAL_TIME, "")) - departure = dt_util.parse_time(config_entry.options.get(CONF_DEPARTURE_TIME, "")) - - here_travel_time_config = HERETravelTimeConfig( - destination_latitude=config_entry.data.get(CONF_DESTINATION_LATITUDE), - destination_longitude=config_entry.data.get(CONF_DESTINATION_LONGITUDE), - destination_entity_id=config_entry.data.get(CONF_DESTINATION_ENTITY_ID), - origin_latitude=config_entry.data.get(CONF_ORIGIN_LATITUDE), - origin_longitude=config_entry.data.get(CONF_ORIGIN_LONGITUDE), - origin_entity_id=config_entry.data.get(CONF_ORIGIN_ENTITY_ID), - travel_mode=config_entry.data[CONF_MODE], - route_mode=config_entry.options[CONF_ROUTE_MODE], - arrival=arrival, - departure=departure, - ) - cls: type[HERETransitDataUpdateCoordinator | HERERoutingDataUpdateCoordinator] if config_entry.data[CONF_MODE] in {TRAVEL_MODE_PUBLIC, "publicTransportTimeTable"}: cls = HERETransitDataUpdateCoordinator else: cls = HERERoutingDataUpdateCoordinator - data_coordinator = cls(hass, config_entry, api_key, here_travel_time_config) + data_coordinator = cls(hass, config_entry, api_key) config_entry.runtime_data = data_coordinator async def _async_update_at_start(_: HomeAssistant) -> None: diff --git a/homeassistant/components/here_travel_time/coordinator.py b/homeassistant/components/here_travel_time/coordinator.py index aa36404c584..447a45f5d2b 100644 --- a/homeassistant/components/here_travel_time/coordinator.py +++ b/homeassistant/components/here_travel_time/coordinator.py @@ -26,7 +26,7 @@ from here_transit import ( import voluptuous as vol from homeassistant.config_entries import ConfigEntry -from homeassistant.const import UnitOfLength +from homeassistant.const import CONF_MODE, UnitOfLength from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.location import find_coordinates @@ -34,8 +34,21 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from homeassistant.util import dt as dt_util from homeassistant.util.unit_conversion import DistanceConverter -from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, ROUTE_MODE_FASTEST -from .model import HERETravelTimeConfig, HERETravelTimeData +from .const import ( + CONF_ARRIVAL_TIME, + CONF_DEPARTURE_TIME, + CONF_DESTINATION_ENTITY_ID, + CONF_DESTINATION_LATITUDE, + CONF_DESTINATION_LONGITUDE, + CONF_ORIGIN_ENTITY_ID, + CONF_ORIGIN_LATITUDE, + CONF_ORIGIN_LONGITUDE, + CONF_ROUTE_MODE, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + ROUTE_MODE_FASTEST, +) +from .model import HERETravelTimeAPIParams, HERETravelTimeData BACKOFF_MULTIPLIER = 1.1 @@ -47,7 +60,7 @@ type HereConfigEntry = ConfigEntry[ class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData]): - """here_routing DataUpdateCoordinator.""" + """HERETravelTime DataUpdateCoordinator for the routing API.""" config_entry: HereConfigEntry @@ -56,7 +69,6 @@ class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData] hass: HomeAssistant, config_entry: HereConfigEntry, api_key: str, - config: HERETravelTimeConfig, ) -> None: """Initialize.""" super().__init__( @@ -67,41 +79,34 @@ class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData] update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL), ) self._api = HERERoutingApi(api_key) - self.config = config async def _async_update_data(self) -> HERETravelTimeData: """Get the latest data from the HERE Routing API.""" - origin, destination, arrival, departure = prepare_parameters( - self.hass, self.config - ) - - route_mode = ( - RoutingMode.FAST - if self.config.route_mode == ROUTE_MODE_FASTEST - else RoutingMode.SHORT - ) + params = prepare_parameters(self.hass, self.config_entry) _LOGGER.debug( ( "Requesting route for origin: %s, destination: %s, route_mode: %s," " mode: %s, arrival: %s, departure: %s" ), - origin, - destination, - route_mode, - TransportMode(self.config.travel_mode), - arrival, - departure, + params.origin, + params.destination, + params.route_mode, + TransportMode(params.travel_mode), + params.arrival, + params.departure, ) try: response = await self._api.route( - transport_mode=TransportMode(self.config.travel_mode), - origin=here_routing.Place(origin[0], origin[1]), - destination=here_routing.Place(destination[0], destination[1]), - routing_mode=route_mode, - arrival_time=arrival, - departure_time=departure, + transport_mode=TransportMode(params.travel_mode), + origin=here_routing.Place(params.origin[0], params.origin[1]), + destination=here_routing.Place( + params.destination[0], params.destination[1] + ), + routing_mode=params.route_mode, + arrival_time=params.arrival, + departure_time=params.departure, return_values=[Return.POLYINE, Return.SUMMARY], spans=[Spans.NAMES], ) @@ -175,7 +180,7 @@ class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData] class HERETransitDataUpdateCoordinator( DataUpdateCoordinator[HERETravelTimeData | None] ): - """HERETravelTime DataUpdateCoordinator.""" + """HERETravelTime DataUpdateCoordinator for the transit API.""" config_entry: HereConfigEntry @@ -184,7 +189,6 @@ class HERETransitDataUpdateCoordinator( hass: HomeAssistant, config_entry: HereConfigEntry, api_key: str, - config: HERETravelTimeConfig, ) -> None: """Initialize.""" super().__init__( @@ -195,32 +199,31 @@ class HERETransitDataUpdateCoordinator( update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL), ) self._api = HERETransitApi(api_key) - self.config = config async def _async_update_data(self) -> HERETravelTimeData | None: """Get the latest data from the HERE Routing API.""" - origin, destination, arrival, departure = prepare_parameters( - self.hass, self.config - ) + params = prepare_parameters(self.hass, self.config_entry) _LOGGER.debug( ( "Requesting transit route for origin: %s, destination: %s, arrival: %s," " departure: %s" ), - origin, - destination, - arrival, - departure, + params.origin, + params.destination, + params.arrival, + params.departure, ) try: response = await self._api.route( - origin=here_transit.Place(latitude=origin[0], longitude=origin[1]), - destination=here_transit.Place( - latitude=destination[0], longitude=destination[1] + origin=here_transit.Place( + latitude=params.origin[0], longitude=params.origin[1] ), - arrival_time=arrival, - departure_time=departure, + destination=here_transit.Place( + latitude=params.destination[0], longitude=params.destination[1] + ), + arrival_time=params.arrival, + departure_time=params.departure, return_values=[ here_transit.Return.POLYLINE, here_transit.Return.TRAVEL_SUMMARY, @@ -285,8 +288,8 @@ class HERETransitDataUpdateCoordinator( def prepare_parameters( hass: HomeAssistant, - config: HERETravelTimeConfig, -) -> tuple[list[str], list[str], str | None, str | None]: + config_entry: HereConfigEntry, +) -> HERETravelTimeAPIParams: """Prepare parameters for the HERE api.""" def _from_entity_id(entity_id: str) -> list[str]: @@ -305,32 +308,55 @@ def prepare_parameters( return formatted_coordinates # Destination - if config.destination_entity_id is not None: - destination = _from_entity_id(config.destination_entity_id) + if ( + destination_entity_id := config_entry.data.get(CONF_DESTINATION_ENTITY_ID) + ) is not None: + destination = _from_entity_id(str(destination_entity_id)) else: destination = [ - str(config.destination_latitude), - str(config.destination_longitude), + str(config_entry.data[CONF_DESTINATION_LATITUDE]), + str(config_entry.data[CONF_DESTINATION_LONGITUDE]), ] # Origin - if config.origin_entity_id is not None: - origin = _from_entity_id(config.origin_entity_id) + if (origin_entity_id := config_entry.data.get(CONF_ORIGIN_ENTITY_ID)) is not None: + origin = _from_entity_id(str(origin_entity_id)) else: origin = [ - str(config.origin_latitude), - str(config.origin_longitude), + str(config_entry.data[CONF_ORIGIN_LATITUDE]), + str(config_entry.data[CONF_ORIGIN_LONGITUDE]), ] # Arrival/Departure - arrival: str | None = None - departure: str | None = None - if config.arrival is not None: - arrival = next_datetime(config.arrival).isoformat() - if config.departure is not None: - departure = next_datetime(config.departure).isoformat() + arrival: datetime | None = None + if ( + conf_arrival := dt_util.parse_time( + config_entry.options.get(CONF_ARRIVAL_TIME, "") + ) + ) is not None: + arrival = next_datetime(conf_arrival) + departure: datetime | None = None + if ( + conf_departure := dt_util.parse_time( + config_entry.options.get(CONF_DEPARTURE_TIME, "") + ) + ) is not None: + departure = next_datetime(conf_departure) - return (origin, destination, arrival, departure) + route_mode = ( + RoutingMode.FAST + if config_entry.options[CONF_ROUTE_MODE] == ROUTE_MODE_FASTEST + else RoutingMode.SHORT + ) + + return HERETravelTimeAPIParams( + destination=destination, + origin=origin, + travel_mode=config_entry.data[CONF_MODE], + route_mode=route_mode, + arrival=arrival, + departure=departure, + ) def build_hass_attribution(sections: list[dict[str, Any]]) -> str | None: diff --git a/homeassistant/components/here_travel_time/model.py b/homeassistant/components/here_travel_time/model.py index 178c0d8c805..cbac2b1c353 100644 --- a/homeassistant/components/here_travel_time/model.py +++ b/homeassistant/components/here_travel_time/model.py @@ -3,7 +3,7 @@ from __future__ import annotations from dataclasses import dataclass -from datetime import time +from datetime import datetime from typing import TypedDict @@ -21,16 +21,12 @@ class HERETravelTimeData(TypedDict): @dataclass -class HERETravelTimeConfig: - """Configuration for HereTravelTimeDataUpdateCoordinator.""" +class HERETravelTimeAPIParams: + """Configuration for polling the HERE API.""" - destination_latitude: float | None - destination_longitude: float | None - destination_entity_id: str | None - origin_latitude: float | None - origin_longitude: float | None - origin_entity_id: str | None + destination: list[str] + origin: list[str] travel_mode: str route_mode: str - arrival: time | None - departure: time | None + arrival: datetime | None + departure: datetime | None From 0cff7cbccde6dbc9bb1f2fd613146ec5beddecfa Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Wed, 11 Jun 2025 18:39:49 +0300 Subject: [PATCH 174/266] Remove stale Shelly BLU TRV devices (#145994) * Remove stale Shelly BLU TRV devices * Add test * Remove config entry from device --- homeassistant/components/shelly/__init__.py | 2 + .../components/shelly/quality_scale.yaml | 4 +- homeassistant/components/shelly/utils.py | 30 ++++++++++++ tests/components/shelly/conftest.py | 49 +++++++++++++++++++ tests/components/shelly/test_init.py | 49 ++++++++++++++++++- 5 files changed, 131 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 3130acff538..75fedf9b16d 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -64,6 +64,7 @@ from .utils import ( get_http_port, get_rpc_scripts_event_types, get_ws_context, + remove_stale_blu_trv_devices, ) PLATFORMS: Final = [ @@ -300,6 +301,7 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ShellyConfigEntry) runtime_data.rpc_script_events = await get_rpc_scripts_event_types( device, ignore_scripts=[BLE_SCRIPT_NAME] ) + remove_stale_blu_trv_devices(hass, device, entry) except (DeviceConnectionError, MacAddressMismatchError, RpcCallError) as err: await device.shutdown() raise ConfigEntryNotReady( diff --git a/homeassistant/components/shelly/quality_scale.yaml b/homeassistant/components/shelly/quality_scale.yaml index 753b2ee4a93..39667b556dd 100644 --- a/homeassistant/components/shelly/quality_scale.yaml +++ b/homeassistant/components/shelly/quality_scale.yaml @@ -61,8 +61,8 @@ rules: reconfiguration-flow: done repair-issues: done stale-devices: - status: todo - comment: BLU TRV needs to be removed when un-paired + status: done + comment: BLU TRV is removed when un-paired # Platinum async-dependency: done diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index eff5c95125c..cc0f2cf75d5 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -16,6 +16,7 @@ from aioshelly.const import ( DEFAULT_COAP_PORT, DEFAULT_HTTP_PORT, MODEL_1L, + MODEL_BLU_GATEWAY_G3, MODEL_DIMMER, MODEL_DIMMER_2, MODEL_EM3, @@ -821,3 +822,32 @@ def get_block_device_info( manufacturer="Shelly", via_device=(DOMAIN, mac), ) + + +@callback +def remove_stale_blu_trv_devices( + hass: HomeAssistant, rpc_device: RpcDevice, entry: ConfigEntry +) -> None: + """Remove stale BLU TRV devices.""" + if rpc_device.model != MODEL_BLU_GATEWAY_G3: + return + + dev_reg = dr.async_get(hass) + devices = dev_reg.devices.get_devices_for_config_entry_id(entry.entry_id) + config = rpc_device.config + blutrv_keys = get_rpc_key_ids(config, BLU_TRV_IDENTIFIER) + trv_addrs = [config[f"{BLU_TRV_IDENTIFIER}:{key}"]["addr"] for key in blutrv_keys] + + for device in devices: + if not device.via_device_id: + # Device is not a sub-device, skip + continue + + if any( + identifier[0] == DOMAIN and identifier[1] in trv_addrs + for identifier in device.identifiers + ): + continue + + LOGGER.debug("Removing stale BLU TRV device %s", device.name) + dev_reg.async_update_device(device.id, remove_config_entry_id=entry.entry_id) diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py index ac70226a20a..4eccb075b67 100644 --- a/tests/components/shelly/conftest.py +++ b/tests/components/shelly/conftest.py @@ -260,6 +260,33 @@ MOCK_BLU_TRV_REMOTE_CONFIG = { "meta": {}, }, }, + { + "key": "blutrv:201", + "status": { + "id": 201, + "target_C": 17.1, + "current_C": 17.1, + "pos": 0, + "rssi": -60, + "battery": 100, + "packet_id": 58, + "last_updated_ts": 1734967725, + "paired": True, + "rpc": True, + "rsv": 61, + }, + "config": { + "id": 201, + "addr": "f8:44:77:25:f0:de", + "name": "TRV-201", + "key": None, + "trv": "bthomedevice:201", + "temp_sensors": [], + "dw_sensors": [], + "override_delay": 30, + "meta": {}, + }, + }, ], "blutrv:200": { "id": 0, @@ -272,6 +299,17 @@ MOCK_BLU_TRV_REMOTE_CONFIG = { "name": "TRV-Name", "local_name": "SBTR-001AEU", }, + "blutrv:201": { + "id": 1, + "enable": True, + "min_valve_position": 0, + "default_boost_duration": 1800, + "default_override_duration": 2147483647, + "default_override_target_C": 8, + "addr": "f8:44:77:25:f0:de", + "name": "TRV-201", + "local_name": "SBTR-001AEU", + }, } @@ -287,6 +325,17 @@ MOCK_BLU_TRV_REMOTE_STATUS = { "battery": 100, "errors": [], }, + "blutrv:201": { + "id": 0, + "pos": 0, + "steps": 0, + "current_C": 15.2, + "target_C": 17.1, + "schedule_rev": 0, + "rssi": -60, + "battery": 100, + "errors": [], + }, } diff --git a/tests/components/shelly/test_init.py b/tests/components/shelly/test_init.py index 283de897d8d..703df09bb61 100644 --- a/tests/components/shelly/test_init.py +++ b/tests/components/shelly/test_init.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, Mock, call, patch from aioshelly.block_device import COAP from aioshelly.common import ConnectionOptions -from aioshelly.const import MODEL_PLUS_2PM +from aioshelly.const import MODEL_BLU_GATEWAY_G3, MODEL_PLUS_2PM from aioshelly.exceptions import ( DeviceConnectionError, InvalidAuthError, @@ -38,6 +38,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.device_registry import DeviceRegistry, format_mac +from homeassistant.helpers.entity_registry import EntityRegistry from homeassistant.setup import async_setup_component from . import MOCK_MAC, init_integration, mutate_rpc_device_status @@ -606,3 +607,49 @@ async def test_ble_scanner_unsupported_firmware_fixed( assert not issue_registry.async_get_issue(DOMAIN, issue_id) assert len(issue_registry.issues) == 0 + + +async def test_blu_trv_stale_device_removal( + hass: HomeAssistant, + mock_blu_trv: Mock, + entity_registry: EntityRegistry, + device_registry: DeviceRegistry, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test BLU TRV removal of stale a device after un-pairing.""" + trv_200_entity_id = "climate.trv_name" + trv_201_entity_id = "climate.trv_201" + + monkeypatch.setattr(mock_blu_trv, "model", MODEL_BLU_GATEWAY_G3) + gw_entry = await init_integration(hass, 3, model=MODEL_BLU_GATEWAY_G3) + + # verify that both trv devices are present + assert hass.states.get(trv_200_entity_id) is not None + trv_200_entry = entity_registry.async_get(trv_200_entity_id) + assert trv_200_entry + + trv_200_device_entry = device_registry.async_get(trv_200_entry.device_id) + assert trv_200_device_entry + assert trv_200_device_entry.name == "TRV-Name" + + assert hass.states.get(trv_201_entity_id) is not None + trv_201_entry = entity_registry.async_get(trv_201_entity_id) + assert trv_201_entry + + trv_201_device_entry = device_registry.async_get(trv_201_entry.device_id) + assert trv_201_device_entry + assert trv_201_device_entry.name == "TRV-201" + + # simulate un-pairing of trv 201 device + monkeypatch.delitem(mock_blu_trv.config, "blutrv:201") + monkeypatch.delitem(mock_blu_trv.status, "blutrv:201") + + await hass.config_entries.async_reload(gw_entry.entry_id) + await hass.async_block_till_done() + + # verify that trv 201 is removed + assert hass.states.get(trv_200_entity_id) is not None + assert device_registry.async_get(trv_200_entry.device_id) is not None + + assert hass.states.get(trv_201_entity_id) is None + assert device_registry.async_get(trv_201_entry.device_id) is None From af72d1854f3a8763a1b65656c6a5841396c4ffe8 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Wed, 11 Jun 2025 15:24:37 +0100 Subject: [PATCH 175/266] Add guide for Honeywell Lyric application credentials setup (#146281) * Add guide for Honeywell Lyric application credentials setup * Fix --------- Co-authored-by: Joostlek --- homeassistant/components/lyric/application_credentials.py | 8 ++++++++ homeassistant/components/lyric/strings.json | 5 ++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/lyric/application_credentials.py b/homeassistant/components/lyric/application_credentials.py index 2ccdca72bb6..9c53395bb6d 100644 --- a/homeassistant/components/lyric/application_credentials.py +++ b/homeassistant/components/lyric/application_credentials.py @@ -24,3 +24,11 @@ async def async_get_auth_implementation( token_url=OAUTH2_TOKEN, ), ) + + +async def async_get_description_placeholders(hass: HomeAssistant) -> dict[str, str]: + """Return description placeholders for the credentials dialog.""" + return { + "developer_dashboard_url": "https://developer.honeywellhome.com", + "redirect_url": "https://my.home-assistant.io/redirect/oauth", + } diff --git a/homeassistant/components/lyric/strings.json b/homeassistant/components/lyric/strings.json index 41598dfbdd0..786f49e5300 100644 --- a/homeassistant/components/lyric/strings.json +++ b/homeassistant/components/lyric/strings.json @@ -1,4 +1,7 @@ { + "application_credentials": { + "description": "To be able to log in to Honeywell Lyric the integration requires a client ID and secret. To acquire those, please follow the following steps.\n\n1. Go to the [Honeywell Lyric Developer Apps Dashboard]({developer_dashboard_url}).\n1. Sign up for a developer account if you don't have one yet. This is a separate account from your Honeywell account.\n1. Log in with your Honeywell Lyric developer account.\n1. Go to the **My Apps** section.\n1. Press the **CREATE NEW APP** button.\n1. Give the application a name of your choice.\n1. Set the **Callback URL** to `{redirect_url}`.\n1. Save your changes.\\n1. Copy the **Consumer Key** and paste it here as the **Client ID**, then copy the **Consumer Secret** and paste it here as the **Client Secret**." + }, "config": { "step": { "pick_implementation": { @@ -9,7 +12,7 @@ "description": "The Lyric integration needs to re-authenticate your account." }, "oauth_discovery": { - "description": "Home Assistant has found a Honeywell Lyric device on your network. Press **Submit** to continue setting up Honeywell Lyric." + "description": "Home Assistant has found a Honeywell Lyric device on your network. Be aware that the setup of the Lyric integration is more complicated than other integrations. Press **Submit** to continue setting up Honeywell Lyric." } }, "abort": { From 82de2ed8e184ea89bca1d99e22fef29a9f6b7d60 Mon Sep 17 00:00:00 2001 From: tronikos Date: Wed, 11 Jun 2025 09:35:26 -0700 Subject: [PATCH 176/266] Rename Amazon Devices to Alexa Devices (#146362) Co-authored-by: Simone Chemelli Co-authored-by: Joostlek --- .strict-typing | 2 +- CODEOWNERS | 4 ++-- homeassistant/brands/amazon.json | 2 +- .../{amazon_devices => alexa_devices}/__init__.py | 4 ++-- .../binary_sensor.py | 4 ++-- .../{amazon_devices => alexa_devices}/config_flow.py | 4 ++-- .../{amazon_devices => alexa_devices}/const.py | 4 ++-- .../{amazon_devices => alexa_devices}/coordinator.py | 4 ++-- .../{amazon_devices => alexa_devices}/diagnostics.py | 2 +- .../{amazon_devices => alexa_devices}/entity.py | 4 ++-- .../{amazon_devices => alexa_devices}/icons.json | 0 .../{amazon_devices => alexa_devices}/manifest.json | 6 +++--- .../{amazon_devices => alexa_devices}/notify.py | 4 ++-- .../quality_scale.yaml | 0 .../{amazon_devices => alexa_devices}/strings.json | 12 ++++++------ .../{amazon_devices => alexa_devices}/switch.py | 4 ++-- homeassistant/generated/config_flows.py | 2 +- homeassistant/generated/integrations.json | 4 ++-- mypy.ini | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../{amazon_devices => alexa_devices}/__init__.py | 2 +- .../{amazon_devices => alexa_devices}/conftest.py | 12 ++++++------ .../{amazon_devices => alexa_devices}/const.py | 2 +- .../snapshots/test_binary_sensor.ambr | 4 ++-- .../snapshots/test_diagnostics.ambr | 2 +- .../snapshots/test_init.ambr | 2 +- .../snapshots/test_notify.ambr | 4 ++-- .../snapshots/test_switch.ambr | 2 +- .../test_binary_sensor.py | 6 +++--- .../test_config_flow.py | 4 ++-- .../test_diagnostics.py | 4 ++-- .../{amazon_devices => alexa_devices}/test_init.py | 4 ++-- .../{amazon_devices => alexa_devices}/test_notify.py | 6 +++--- .../{amazon_devices => alexa_devices}/test_switch.py | 6 +++--- 35 files changed, 67 insertions(+), 67 deletions(-) rename homeassistant/components/{amazon_devices => alexa_devices}/__init__.py (91%) rename homeassistant/components/{amazon_devices => alexa_devices}/binary_sensor.py (93%) rename homeassistant/components/{amazon_devices => alexa_devices}/config_flow.py (95%) rename homeassistant/components/{amazon_devices => alexa_devices}/const.py (60%) rename homeassistant/components/{amazon_devices => alexa_devices}/coordinator.py (95%) rename homeassistant/components/{amazon_devices => alexa_devices}/diagnostics.py (97%) rename homeassistant/components/{amazon_devices => alexa_devices}/entity.py (95%) rename homeassistant/components/{amazon_devices => alexa_devices}/icons.json (100%) rename homeassistant/components/{amazon_devices => alexa_devices}/manifest.json (79%) rename homeassistant/components/{amazon_devices => alexa_devices}/notify.py (94%) rename homeassistant/components/{amazon_devices => alexa_devices}/quality_scale.yaml (100%) rename homeassistant/components/{amazon_devices => alexa_devices}/strings.json (75%) rename homeassistant/components/{amazon_devices => alexa_devices}/switch.py (95%) rename tests/components/{amazon_devices => alexa_devices}/__init__.py (88%) rename tests/components/{amazon_devices => alexa_devices}/conftest.py (84%) rename tests/components/{amazon_devices => alexa_devices}/const.py (82%) rename tests/components/{amazon_devices => alexa_devices}/snapshots/test_binary_sensor.ambr (97%) rename tests/components/{amazon_devices => alexa_devices}/snapshots/test_diagnostics.ambr (98%) rename tests/components/{amazon_devices => alexa_devices}/snapshots/test_init.ambr (96%) rename tests/components/{amazon_devices => alexa_devices}/snapshots/test_notify.ambr (97%) rename tests/components/{amazon_devices => alexa_devices}/snapshots/test_switch.ambr (97%) rename tests/components/{amazon_devices => alexa_devices}/test_binary_sensor.py (92%) rename tests/components/{amazon_devices => alexa_devices}/test_config_flow.py (96%) rename tests/components/{amazon_devices => alexa_devices}/test_diagnostics.py (93%) rename tests/components/{amazon_devices => alexa_devices}/test_init.py (87%) rename tests/components/{amazon_devices => alexa_devices}/test_notify.py (92%) rename tests/components/{amazon_devices => alexa_devices}/test_switch.py (94%) diff --git a/.strict-typing b/.strict-typing index 4febfd68486..b34cbfa5fca 100644 --- a/.strict-typing +++ b/.strict-typing @@ -65,8 +65,8 @@ homeassistant.components.aladdin_connect.* homeassistant.components.alarm_control_panel.* homeassistant.components.alert.* homeassistant.components.alexa.* +homeassistant.components.alexa_devices.* homeassistant.components.alpha_vantage.* -homeassistant.components.amazon_devices.* homeassistant.components.amazon_polly.* homeassistant.components.amberelectric.* homeassistant.components.ambient_network.* diff --git a/CODEOWNERS b/CODEOWNERS index 3f3ce07ce84..b447c878128 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -89,8 +89,8 @@ build.json @home-assistant/supervisor /tests/components/alert/ @home-assistant/core @frenck /homeassistant/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh /tests/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh -/homeassistant/components/amazon_devices/ @chemelli74 -/tests/components/amazon_devices/ @chemelli74 +/homeassistant/components/alexa_devices/ @chemelli74 +/tests/components/alexa_devices/ @chemelli74 /homeassistant/components/amazon_polly/ @jschlyter /homeassistant/components/amberelectric/ @madpilot /tests/components/amberelectric/ @madpilot diff --git a/homeassistant/brands/amazon.json b/homeassistant/brands/amazon.json index d2e25468388..126b69c848d 100644 --- a/homeassistant/brands/amazon.json +++ b/homeassistant/brands/amazon.json @@ -3,7 +3,7 @@ "name": "Amazon", "integrations": [ "alexa", - "amazon_devices", + "alexa_devices", "amazon_polly", "aws", "aws_s3", diff --git a/homeassistant/components/amazon_devices/__init__.py b/homeassistant/components/alexa_devices/__init__.py similarity index 91% rename from homeassistant/components/amazon_devices/__init__.py rename to homeassistant/components/alexa_devices/__init__.py index 1db41d335ef..7a4139a65da 100644 --- a/homeassistant/components/amazon_devices/__init__.py +++ b/homeassistant/components/alexa_devices/__init__.py @@ -1,4 +1,4 @@ -"""Amazon Devices integration.""" +"""Alexa Devices integration.""" from homeassistant.const import Platform from homeassistant.core import HomeAssistant @@ -13,7 +13,7 @@ PLATFORMS = [ async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool: - """Set up Amazon Devices platform.""" + """Set up Alexa Devices platform.""" coordinator = AmazonDevicesCoordinator(hass, entry) diff --git a/homeassistant/components/amazon_devices/binary_sensor.py b/homeassistant/components/alexa_devices/binary_sensor.py similarity index 93% rename from homeassistant/components/amazon_devices/binary_sensor.py rename to homeassistant/components/alexa_devices/binary_sensor.py index ab1fadc7548..16cf73aee9f 100644 --- a/homeassistant/components/amazon_devices/binary_sensor.py +++ b/homeassistant/components/alexa_devices/binary_sensor.py @@ -26,7 +26,7 @@ PARALLEL_UPDATES = 0 @dataclass(frozen=True, kw_only=True) class AmazonBinarySensorEntityDescription(BinarySensorEntityDescription): - """Amazon Devices binary sensor entity description.""" + """Alexa Devices binary sensor entity description.""" is_on_fn: Callable[[AmazonDevice], bool] @@ -52,7 +52,7 @@ async def async_setup_entry( entry: AmazonConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: - """Set up Amazon Devices binary sensors based on a config entry.""" + """Set up Alexa Devices binary sensors based on a config entry.""" coordinator = entry.runtime_data diff --git a/homeassistant/components/amazon_devices/config_flow.py b/homeassistant/components/alexa_devices/config_flow.py similarity index 95% rename from homeassistant/components/amazon_devices/config_flow.py rename to homeassistant/components/alexa_devices/config_flow.py index d0c3d067cee..5add7ceb711 100644 --- a/homeassistant/components/amazon_devices/config_flow.py +++ b/homeassistant/components/alexa_devices/config_flow.py @@ -1,4 +1,4 @@ -"""Config flow for Amazon Devices integration.""" +"""Config flow for Alexa Devices integration.""" from __future__ import annotations @@ -17,7 +17,7 @@ from .const import CONF_LOGIN_DATA, DOMAIN class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN): - """Handle a config flow for Amazon Devices.""" + """Handle a config flow for Alexa Devices.""" async def async_step_user( self, user_input: dict[str, Any] | None = None diff --git a/homeassistant/components/amazon_devices/const.py b/homeassistant/components/alexa_devices/const.py similarity index 60% rename from homeassistant/components/amazon_devices/const.py rename to homeassistant/components/alexa_devices/const.py index b8cf2c264b1..ca0290a10bc 100644 --- a/homeassistant/components/amazon_devices/const.py +++ b/homeassistant/components/alexa_devices/const.py @@ -1,8 +1,8 @@ -"""Amazon Devices constants.""" +"""Alexa Devices constants.""" import logging _LOGGER = logging.getLogger(__package__) -DOMAIN = "amazon_devices" +DOMAIN = "alexa_devices" CONF_LOGIN_DATA = "login_data" diff --git a/homeassistant/components/amazon_devices/coordinator.py b/homeassistant/components/alexa_devices/coordinator.py similarity index 95% rename from homeassistant/components/amazon_devices/coordinator.py rename to homeassistant/components/alexa_devices/coordinator.py index 48e31cb3f94..8e58441d46c 100644 --- a/homeassistant/components/amazon_devices/coordinator.py +++ b/homeassistant/components/alexa_devices/coordinator.py @@ -1,4 +1,4 @@ -"""Support for Amazon Devices.""" +"""Support for Alexa Devices.""" from datetime import timedelta @@ -23,7 +23,7 @@ type AmazonConfigEntry = ConfigEntry[AmazonDevicesCoordinator] class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]): - """Base coordinator for Amazon Devices.""" + """Base coordinator for Alexa Devices.""" config_entry: AmazonConfigEntry diff --git a/homeassistant/components/amazon_devices/diagnostics.py b/homeassistant/components/alexa_devices/diagnostics.py similarity index 97% rename from homeassistant/components/amazon_devices/diagnostics.py rename to homeassistant/components/alexa_devices/diagnostics.py index e9a0773cd3f..0c4cb794416 100644 --- a/homeassistant/components/amazon_devices/diagnostics.py +++ b/homeassistant/components/alexa_devices/diagnostics.py @@ -1,4 +1,4 @@ -"""Diagnostics support for Amazon Devices integration.""" +"""Diagnostics support for Alexa Devices integration.""" from __future__ import annotations diff --git a/homeassistant/components/amazon_devices/entity.py b/homeassistant/components/alexa_devices/entity.py similarity index 95% rename from homeassistant/components/amazon_devices/entity.py rename to homeassistant/components/alexa_devices/entity.py index 962e2f55ae6..f539079602f 100644 --- a/homeassistant/components/amazon_devices/entity.py +++ b/homeassistant/components/alexa_devices/entity.py @@ -1,4 +1,4 @@ -"""Defines a base Amazon Devices entity.""" +"""Defines a base Alexa Devices entity.""" from aioamazondevices.api import AmazonDevice from aioamazondevices.const import SPEAKER_GROUP_MODEL @@ -12,7 +12,7 @@ from .coordinator import AmazonDevicesCoordinator class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]): - """Defines a base Amazon Devices entity.""" + """Defines a base Alexa Devices entity.""" _attr_has_entity_name = True diff --git a/homeassistant/components/amazon_devices/icons.json b/homeassistant/components/alexa_devices/icons.json similarity index 100% rename from homeassistant/components/amazon_devices/icons.json rename to homeassistant/components/alexa_devices/icons.json diff --git a/homeassistant/components/amazon_devices/manifest.json b/homeassistant/components/alexa_devices/manifest.json similarity index 79% rename from homeassistant/components/amazon_devices/manifest.json rename to homeassistant/components/alexa_devices/manifest.json index f63893c1598..2a9e88cfd85 100644 --- a/homeassistant/components/amazon_devices/manifest.json +++ b/homeassistant/components/alexa_devices/manifest.json @@ -1,9 +1,9 @@ { - "domain": "amazon_devices", - "name": "Amazon Devices", + "domain": "alexa_devices", + "name": "Alexa Devices", "codeowners": ["@chemelli74"], "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/amazon_devices", + "documentation": "https://www.home-assistant.io/integrations/alexa_devices", "integration_type": "hub", "iot_class": "cloud_polling", "loggers": ["aioamazondevices"], diff --git a/homeassistant/components/amazon_devices/notify.py b/homeassistant/components/alexa_devices/notify.py similarity index 94% rename from homeassistant/components/amazon_devices/notify.py rename to homeassistant/components/alexa_devices/notify.py index 3762a7a3264..ff0cd4e59ea 100644 --- a/homeassistant/components/amazon_devices/notify.py +++ b/homeassistant/components/alexa_devices/notify.py @@ -20,7 +20,7 @@ PARALLEL_UPDATES = 1 @dataclass(frozen=True, kw_only=True) class AmazonNotifyEntityDescription(NotifyEntityDescription): - """Amazon Devices notify entity description.""" + """Alexa Devices notify entity description.""" method: Callable[[AmazonEchoApi, AmazonDevice, str], Awaitable[None]] subkey: str @@ -49,7 +49,7 @@ async def async_setup_entry( entry: AmazonConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: - """Set up Amazon Devices notification entity based on a config entry.""" + """Set up Alexa Devices notification entity based on a config entry.""" coordinator = entry.runtime_data diff --git a/homeassistant/components/amazon_devices/quality_scale.yaml b/homeassistant/components/alexa_devices/quality_scale.yaml similarity index 100% rename from homeassistant/components/amazon_devices/quality_scale.yaml rename to homeassistant/components/alexa_devices/quality_scale.yaml diff --git a/homeassistant/components/amazon_devices/strings.json b/homeassistant/components/alexa_devices/strings.json similarity index 75% rename from homeassistant/components/amazon_devices/strings.json rename to homeassistant/components/alexa_devices/strings.json index 47e6234cd9c..9d615b248ed 100644 --- a/homeassistant/components/amazon_devices/strings.json +++ b/homeassistant/components/alexa_devices/strings.json @@ -12,16 +12,16 @@ "step": { "user": { "data": { - "country": "[%key:component::amazon_devices::common::data_country%]", + "country": "[%key:component::alexa_devices::common::data_country%]", "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]", - "code": "[%key:component::amazon_devices::common::data_description_code%]" + "code": "[%key:component::alexa_devices::common::data_description_code%]" }, "data_description": { - "country": "[%key:component::amazon_devices::common::data_description_country%]", - "username": "[%key:component::amazon_devices::common::data_description_username%]", - "password": "[%key:component::amazon_devices::common::data_description_password%]", - "code": "[%key:component::amazon_devices::common::data_description_code%]" + "country": "[%key:component::alexa_devices::common::data_description_country%]", + "username": "[%key:component::alexa_devices::common::data_description_username%]", + "password": "[%key:component::alexa_devices::common::data_description_password%]", + "code": "[%key:component::alexa_devices::common::data_description_code%]" } } }, diff --git a/homeassistant/components/amazon_devices/switch.py b/homeassistant/components/alexa_devices/switch.py similarity index 95% rename from homeassistant/components/amazon_devices/switch.py rename to homeassistant/components/alexa_devices/switch.py index 428ef3e3b45..b8f78134feb 100644 --- a/homeassistant/components/amazon_devices/switch.py +++ b/homeassistant/components/alexa_devices/switch.py @@ -20,7 +20,7 @@ PARALLEL_UPDATES = 1 @dataclass(frozen=True, kw_only=True) class AmazonSwitchEntityDescription(SwitchEntityDescription): - """Amazon Devices switch entity description.""" + """Alexa Devices switch entity description.""" is_on_fn: Callable[[AmazonDevice], bool] subkey: str @@ -43,7 +43,7 @@ async def async_setup_entry( entry: AmazonConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: - """Set up Amazon Devices switches based on a config entry.""" + """Set up Alexa Devices switches based on a config entry.""" coordinator = entry.runtime_data diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 44a9b19e8c2..2d246f53ca3 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -47,7 +47,7 @@ FLOWS = { "airzone", "airzone_cloud", "alarmdecoder", - "amazon_devices", + "alexa_devices", "amberelectric", "ambient_network", "ambient_station", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 775272f77c4..846a5c74ddb 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -207,11 +207,11 @@ "amazon": { "name": "Amazon", "integrations": { - "amazon_devices": { + "alexa_devices": { "integration_type": "hub", "config_flow": true, "iot_class": "cloud_polling", - "name": "Amazon Devices" + "name": "Alexa Devices" }, "amazon_polly": { "integration_type": "hub", diff --git a/mypy.ini b/mypy.ini index da76e4ae2cd..1fdab75663e 100644 --- a/mypy.ini +++ b/mypy.ini @@ -405,7 +405,7 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true -[mypy-homeassistant.components.alpha_vantage.*] +[mypy-homeassistant.components.alexa_devices.*] check_untyped_defs = true disallow_incomplete_defs = true disallow_subclassing_any = true @@ -415,7 +415,7 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true -[mypy-homeassistant.components.amazon_devices.*] +[mypy-homeassistant.components.alpha_vantage.*] check_untyped_defs = true disallow_incomplete_defs = true disallow_subclassing_any = true diff --git a/requirements_all.txt b/requirements_all.txt index 95a5f919add..d9a9014a6b9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -181,7 +181,7 @@ aioairzone-cloud==0.6.12 # homeassistant.components.airzone aioairzone==1.0.0 -# homeassistant.components.amazon_devices +# homeassistant.components.alexa_devices aioamazondevices==3.0.6 # homeassistant.components.ambient_network diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1ed51e54164..66f34dd6d69 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -169,7 +169,7 @@ aioairzone-cloud==0.6.12 # homeassistant.components.airzone aioairzone==1.0.0 -# homeassistant.components.amazon_devices +# homeassistant.components.alexa_devices aioamazondevices==3.0.6 # homeassistant.components.ambient_network diff --git a/tests/components/amazon_devices/__init__.py b/tests/components/alexa_devices/__init__.py similarity index 88% rename from tests/components/amazon_devices/__init__.py rename to tests/components/alexa_devices/__init__.py index 47ee520b124..24348248e0c 100644 --- a/tests/components/amazon_devices/__init__.py +++ b/tests/components/alexa_devices/__init__.py @@ -1,4 +1,4 @@ -"""Tests for the Amazon Devices integration.""" +"""Tests for the Alexa Devices integration.""" from homeassistant.core import HomeAssistant diff --git a/tests/components/amazon_devices/conftest.py b/tests/components/alexa_devices/conftest.py similarity index 84% rename from tests/components/amazon_devices/conftest.py rename to tests/components/alexa_devices/conftest.py index f0ee29d44e5..4ce2eb743ea 100644 --- a/tests/components/amazon_devices/conftest.py +++ b/tests/components/alexa_devices/conftest.py @@ -1,4 +1,4 @@ -"""Amazon Devices tests configuration.""" +"""Alexa Devices tests configuration.""" from collections.abc import Generator from unittest.mock import AsyncMock, patch @@ -7,7 +7,7 @@ from aioamazondevices.api import AmazonDevice from aioamazondevices.const import DEVICE_TYPE_TO_MODEL import pytest -from homeassistant.components.amazon_devices.const import CONF_LOGIN_DATA, DOMAIN +from homeassistant.components.alexa_devices.const import CONF_LOGIN_DATA, DOMAIN from homeassistant.const import CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME from .const import TEST_COUNTRY, TEST_PASSWORD, TEST_SERIAL_NUMBER, TEST_USERNAME @@ -19,7 +19,7 @@ from tests.common import MockConfigEntry def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( - "homeassistant.components.amazon_devices.async_setup_entry", + "homeassistant.components.alexa_devices.async_setup_entry", return_value=True, ) as mock_setup_entry: yield mock_setup_entry @@ -27,14 +27,14 @@ def mock_setup_entry() -> Generator[AsyncMock]: @pytest.fixture def mock_amazon_devices_client() -> Generator[AsyncMock]: - """Mock an Amazon Devices client.""" + """Mock an Alexa Devices client.""" with ( patch( - "homeassistant.components.amazon_devices.coordinator.AmazonEchoApi", + "homeassistant.components.alexa_devices.coordinator.AmazonEchoApi", autospec=True, ) as mock_client, patch( - "homeassistant.components.amazon_devices.config_flow.AmazonEchoApi", + "homeassistant.components.alexa_devices.config_flow.AmazonEchoApi", new=mock_client, ), ): diff --git a/tests/components/amazon_devices/const.py b/tests/components/alexa_devices/const.py similarity index 82% rename from tests/components/amazon_devices/const.py rename to tests/components/alexa_devices/const.py index a2600ba98a6..8a2f5b6b158 100644 --- a/tests/components/amazon_devices/const.py +++ b/tests/components/alexa_devices/const.py @@ -1,4 +1,4 @@ -"""Amazon Devices tests const.""" +"""Alexa Devices tests const.""" TEST_CODE = "023123" TEST_COUNTRY = "IT" diff --git a/tests/components/amazon_devices/snapshots/test_binary_sensor.ambr b/tests/components/alexa_devices/snapshots/test_binary_sensor.ambr similarity index 97% rename from tests/components/amazon_devices/snapshots/test_binary_sensor.ambr rename to tests/components/alexa_devices/snapshots/test_binary_sensor.ambr index e914541d19c..16f9eeaedae 100644 --- a/tests/components/amazon_devices/snapshots/test_binary_sensor.ambr +++ b/tests/components/alexa_devices/snapshots/test_binary_sensor.ambr @@ -25,7 +25,7 @@ 'original_device_class': None, 'original_icon': None, 'original_name': 'Bluetooth', - 'platform': 'amazon_devices', + 'platform': 'alexa_devices', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, @@ -73,7 +73,7 @@ 'original_device_class': , 'original_icon': None, 'original_name': 'Connectivity', - 'platform': 'amazon_devices', + 'platform': 'alexa_devices', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, diff --git a/tests/components/amazon_devices/snapshots/test_diagnostics.ambr b/tests/components/alexa_devices/snapshots/test_diagnostics.ambr similarity index 98% rename from tests/components/amazon_devices/snapshots/test_diagnostics.ambr rename to tests/components/alexa_devices/snapshots/test_diagnostics.ambr index 0b5164418aa..95798fca817 100644 --- a/tests/components/amazon_devices/snapshots/test_diagnostics.ambr +++ b/tests/components/alexa_devices/snapshots/test_diagnostics.ambr @@ -57,7 +57,7 @@ 'disabled_by': None, 'discovery_keys': dict({ }), - 'domain': 'amazon_devices', + 'domain': 'alexa_devices', 'minor_version': 1, 'options': dict({ }), diff --git a/tests/components/amazon_devices/snapshots/test_init.ambr b/tests/components/alexa_devices/snapshots/test_init.ambr similarity index 96% rename from tests/components/amazon_devices/snapshots/test_init.ambr rename to tests/components/alexa_devices/snapshots/test_init.ambr index be0a5894eea..e0460c4c173 100644 --- a/tests/components/amazon_devices/snapshots/test_init.ambr +++ b/tests/components/alexa_devices/snapshots/test_init.ambr @@ -13,7 +13,7 @@ 'id': , 'identifiers': set({ tuple( - 'amazon_devices', + 'alexa_devices', 'echo_test_serial_number', ), }), diff --git a/tests/components/amazon_devices/snapshots/test_notify.ambr b/tests/components/alexa_devices/snapshots/test_notify.ambr similarity index 97% rename from tests/components/amazon_devices/snapshots/test_notify.ambr rename to tests/components/alexa_devices/snapshots/test_notify.ambr index a47bf7a63ae..64776c14420 100644 --- a/tests/components/amazon_devices/snapshots/test_notify.ambr +++ b/tests/components/alexa_devices/snapshots/test_notify.ambr @@ -25,7 +25,7 @@ 'original_device_class': None, 'original_icon': None, 'original_name': 'Announce', - 'platform': 'amazon_devices', + 'platform': 'alexa_devices', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, @@ -74,7 +74,7 @@ 'original_device_class': None, 'original_icon': None, 'original_name': 'Speak', - 'platform': 'amazon_devices', + 'platform': 'alexa_devices', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, diff --git a/tests/components/amazon_devices/snapshots/test_switch.ambr b/tests/components/alexa_devices/snapshots/test_switch.ambr similarity index 97% rename from tests/components/amazon_devices/snapshots/test_switch.ambr rename to tests/components/alexa_devices/snapshots/test_switch.ambr index 8a2ce8d529a..c622cc67ea7 100644 --- a/tests/components/amazon_devices/snapshots/test_switch.ambr +++ b/tests/components/alexa_devices/snapshots/test_switch.ambr @@ -25,7 +25,7 @@ 'original_device_class': None, 'original_icon': None, 'original_name': 'Do not disturb', - 'platform': 'amazon_devices', + 'platform': 'alexa_devices', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, diff --git a/tests/components/amazon_devices/test_binary_sensor.py b/tests/components/alexa_devices/test_binary_sensor.py similarity index 92% rename from tests/components/amazon_devices/test_binary_sensor.py rename to tests/components/alexa_devices/test_binary_sensor.py index b31d85e06aa..a2e38b3459b 100644 --- a/tests/components/amazon_devices/test_binary_sensor.py +++ b/tests/components/alexa_devices/test_binary_sensor.py @@ -1,4 +1,4 @@ -"""Tests for the Amazon Devices binary sensor platform.""" +"""Tests for the Alexa Devices binary sensor platform.""" from unittest.mock import AsyncMock, patch @@ -11,7 +11,7 @@ from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components.amazon_devices.coordinator import SCAN_INTERVAL +from homeassistant.components.alexa_devices.coordinator import SCAN_INTERVAL from homeassistant.const import STATE_ON, STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -31,7 +31,7 @@ async def test_all_entities( ) -> None: """Test all entities.""" with patch( - "homeassistant.components.amazon_devices.PLATFORMS", [Platform.BINARY_SENSOR] + "homeassistant.components.alexa_devices.PLATFORMS", [Platform.BINARY_SENSOR] ): await setup_integration(hass, mock_config_entry) diff --git a/tests/components/amazon_devices/test_config_flow.py b/tests/components/alexa_devices/test_config_flow.py similarity index 96% rename from tests/components/amazon_devices/test_config_flow.py rename to tests/components/alexa_devices/test_config_flow.py index ce1ac44d102..9bf174c5955 100644 --- a/tests/components/amazon_devices/test_config_flow.py +++ b/tests/components/alexa_devices/test_config_flow.py @@ -1,11 +1,11 @@ -"""Tests for the Amazon Devices config flow.""" +"""Tests for the Alexa Devices config flow.""" from unittest.mock import AsyncMock from aioamazondevices.exceptions import CannotAuthenticate, CannotConnect import pytest -from homeassistant.components.amazon_devices.const import CONF_LOGIN_DATA, DOMAIN +from homeassistant.components.alexa_devices.const import CONF_LOGIN_DATA, DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_CODE, CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant diff --git a/tests/components/amazon_devices/test_diagnostics.py b/tests/components/alexa_devices/test_diagnostics.py similarity index 93% rename from tests/components/amazon_devices/test_diagnostics.py rename to tests/components/alexa_devices/test_diagnostics.py index e548702650b..3c18d432543 100644 --- a/tests/components/amazon_devices/test_diagnostics.py +++ b/tests/components/alexa_devices/test_diagnostics.py @@ -1,4 +1,4 @@ -"""Tests for Amazon Devices diagnostics platform.""" +"""Tests for Alexa Devices diagnostics platform.""" from __future__ import annotations @@ -7,7 +7,7 @@ from unittest.mock import AsyncMock from syrupy.assertion import SnapshotAssertion from syrupy.filters import props -from homeassistant.components.amazon_devices.const import DOMAIN +from homeassistant.components.alexa_devices.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr diff --git a/tests/components/amazon_devices/test_init.py b/tests/components/alexa_devices/test_init.py similarity index 87% rename from tests/components/amazon_devices/test_init.py rename to tests/components/alexa_devices/test_init.py index 489952dbd4c..3100cfe5fa9 100644 --- a/tests/components/amazon_devices/test_init.py +++ b/tests/components/alexa_devices/test_init.py @@ -1,10 +1,10 @@ -"""Tests for the Amazon Devices integration.""" +"""Tests for the Alexa Devices integration.""" from unittest.mock import AsyncMock from syrupy.assertion import SnapshotAssertion -from homeassistant.components.amazon_devices.const import DOMAIN +from homeassistant.components.alexa_devices.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr diff --git a/tests/components/amazon_devices/test_notify.py b/tests/components/alexa_devices/test_notify.py similarity index 92% rename from tests/components/amazon_devices/test_notify.py rename to tests/components/alexa_devices/test_notify.py index b486380fd07..6067874e370 100644 --- a/tests/components/amazon_devices/test_notify.py +++ b/tests/components/alexa_devices/test_notify.py @@ -1,4 +1,4 @@ -"""Tests for the Amazon Devices notify platform.""" +"""Tests for the Alexa Devices notify platform.""" from unittest.mock import AsyncMock, patch @@ -6,7 +6,7 @@ from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components.amazon_devices.coordinator import SCAN_INTERVAL +from homeassistant.components.alexa_devices.coordinator import SCAN_INTERVAL from homeassistant.components.notify import ( ATTR_MESSAGE, DOMAIN as NOTIFY_DOMAIN, @@ -32,7 +32,7 @@ async def test_all_entities( entity_registry: er.EntityRegistry, ) -> None: """Test all entities.""" - with patch("homeassistant.components.amazon_devices.PLATFORMS", [Platform.NOTIFY]): + with patch("homeassistant.components.alexa_devices.PLATFORMS", [Platform.NOTIFY]): await setup_integration(hass, mock_config_entry) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) diff --git a/tests/components/amazon_devices/test_switch.py b/tests/components/alexa_devices/test_switch.py similarity index 94% rename from tests/components/amazon_devices/test_switch.py rename to tests/components/alexa_devices/test_switch.py index 24af96db280..26a18fb731a 100644 --- a/tests/components/amazon_devices/test_switch.py +++ b/tests/components/alexa_devices/test_switch.py @@ -1,4 +1,4 @@ -"""Tests for the Amazon Devices switch platform.""" +"""Tests for the Alexa Devices switch platform.""" from unittest.mock import AsyncMock, patch @@ -6,7 +6,7 @@ from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components.amazon_devices.coordinator import SCAN_INTERVAL +from homeassistant.components.alexa_devices.coordinator import SCAN_INTERVAL from homeassistant.components.switch import ( DOMAIN as SWITCH_DOMAIN, SERVICE_TURN_OFF, @@ -37,7 +37,7 @@ async def test_all_entities( entity_registry: er.EntityRegistry, ) -> None: """Test all entities.""" - with patch("homeassistant.components.amazon_devices.PLATFORMS", [Platform.SWITCH]): + with patch("homeassistant.components.alexa_devices.PLATFORMS", [Platform.SWITCH]): await setup_integration(hass, mock_config_entry) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) From 6384c800c313f27f8a02d6b487156c3c1a9e033d Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 11 Jun 2025 22:46:40 +1200 Subject: [PATCH 177/266] Fix solax state class of `Today's Generated Energy` (#146492) --- homeassistant/components/solax/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/solax/sensor.py b/homeassistant/components/solax/sensor.py index 1cdec0389fe..61420c152a5 100644 --- a/homeassistant/components/solax/sensor.py +++ b/homeassistant/components/solax/sensor.py @@ -42,7 +42,7 @@ SENSOR_DESCRIPTIONS: dict[tuple[Units, bool], SensorEntityDescription] = { key=f"{Units.KWH}_{False}", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL_INCREASING, ), (Units.KWH, True): SensorEntityDescription( key=f"{Units.KWH}_{True}", From e0f32cfd54a068820c6158e5e62337812fe39fa7 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 11 Jun 2025 12:42:09 +0200 Subject: [PATCH 178/266] Allow removing entity registry items twice (#146519) --- homeassistant/helpers/entity_registry.py | 8 ++++++++ tests/helpers/test_entity_registry.py | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index b503ba5f787..72689bc4997 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -980,6 +980,14 @@ class EntityRegistry(BaseRegistry): def async_remove(self, entity_id: str) -> None: """Remove an entity from registry.""" self.hass.verify_event_loop_thread("entity_registry.async_remove") + if entity_id not in self.entities: + # Allow attempts to remove an entity which does not exist. If this is + # not allowed, there will be races during cleanup where we iterate over + # lists of entities to remove, but there are listeners for entity + # registry events which delete entities at the same time. + # For example, if we clean up entities A and B, there might be a listener + # which deletes entity B when entity A is being removed. + return entity = self.entities.pop(entity_id) config_entry_id = entity.config_entry_id key = (entity.domain, entity.platform, entity.unique_id) diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index cef52810fa0..554adff3700 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -289,6 +289,24 @@ def test_get_or_create_suggested_object_id_conflict_existing( assert entry.entity_id == "light.hue_1234_2" +def test_remove(entity_registry: er.EntityRegistry) -> None: + """Test that we can remove an item.""" + entry = entity_registry.async_get_or_create("light", "hue", "1234") + + assert not entity_registry.deleted_entities + assert list(entity_registry.entities) == [entry.entity_id] + + # Remove the item + entity_registry.async_remove(entry.entity_id) + assert list(entity_registry.deleted_entities) == [("light", "hue", "1234")] + assert not entity_registry.entities + + # Remove the item again + entity_registry.async_remove(entry.entity_id) + assert list(entity_registry.deleted_entities) == [("light", "hue", "1234")] + assert not entity_registry.entities + + def test_create_triggers_save(entity_registry: er.EntityRegistry) -> None: """Test that registering entry triggers a save.""" with patch.object(entity_registry, "async_schedule_save") as mock_schedule_save: From cc972d20f64a44045c2127cd1d948fc9755d8241 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 11 Jun 2025 12:31:07 +0200 Subject: [PATCH 179/266] Remove Z-Wave useless reconfigure options (#146520) * Remove emulate hardware option * Remove log level option --- .../components/zwave_js/config_flow.py | 24 -------- homeassistant/components/zwave_js/const.py | 2 - .../components/zwave_js/strings.json | 2 - tests/components/zwave_js/test_config_flow.py | 58 +------------------ 4 files changed, 1 insertion(+), 85 deletions(-) diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index 08c9ec2e2b2..5e8e7022839 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -46,8 +46,6 @@ from .addon import get_addon_manager from .const import ( ADDON_SLUG, CONF_ADDON_DEVICE, - CONF_ADDON_EMULATE_HARDWARE, - CONF_ADDON_LOG_LEVEL, CONF_ADDON_LR_S2_ACCESS_CONTROL_KEY, CONF_ADDON_LR_S2_AUTHENTICATED_KEY, CONF_ADDON_NETWORK_KEY, @@ -78,17 +76,7 @@ TITLE = "Z-Wave JS" ADDON_SETUP_TIMEOUT = 5 ADDON_SETUP_TIMEOUT_ROUNDS = 40 -CONF_EMULATE_HARDWARE = "emulate_hardware" -CONF_LOG_LEVEL = "log_level" -ADDON_LOG_LEVELS = { - "error": "Error", - "warn": "Warn", - "info": "Info", - "verbose": "Verbose", - "debug": "Debug", - "silly": "Silly", -} ADDON_USER_INPUT_MAP = { CONF_ADDON_DEVICE: CONF_USB_PATH, CONF_ADDON_S0_LEGACY_KEY: CONF_S0_LEGACY_KEY, @@ -97,8 +85,6 @@ ADDON_USER_INPUT_MAP = { CONF_ADDON_S2_UNAUTHENTICATED_KEY: CONF_S2_UNAUTHENTICATED_KEY, CONF_ADDON_LR_S2_ACCESS_CONTROL_KEY: CONF_LR_S2_ACCESS_CONTROL_KEY, CONF_ADDON_LR_S2_AUTHENTICATED_KEY: CONF_LR_S2_AUTHENTICATED_KEY, - CONF_ADDON_LOG_LEVEL: CONF_LOG_LEVEL, - CONF_ADDON_EMULATE_HARDWARE: CONF_EMULATE_HARDWARE, } ON_SUPERVISOR_SCHEMA = vol.Schema({vol.Optional(CONF_USE_ADDON, default=True): bool}) @@ -1097,10 +1083,6 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): CONF_ADDON_S2_UNAUTHENTICATED_KEY: self.s2_unauthenticated_key, CONF_ADDON_LR_S2_ACCESS_CONTROL_KEY: self.lr_s2_access_control_key, CONF_ADDON_LR_S2_AUTHENTICATED_KEY: self.lr_s2_authenticated_key, - CONF_ADDON_LOG_LEVEL: user_input[CONF_LOG_LEVEL], - CONF_ADDON_EMULATE_HARDWARE: user_input.get( - CONF_EMULATE_HARDWARE, False - ), } await self._async_set_addon_config(addon_config_updates) @@ -1135,8 +1117,6 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): lr_s2_authenticated_key = addon_config.get( CONF_ADDON_LR_S2_AUTHENTICATED_KEY, self.lr_s2_authenticated_key or "" ) - log_level = addon_config.get(CONF_ADDON_LOG_LEVEL, "info") - emulate_hardware = addon_config.get(CONF_ADDON_EMULATE_HARDWARE, False) try: ports = await async_get_usb_ports(self.hass) @@ -1163,10 +1143,6 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): vol.Optional( CONF_LR_S2_AUTHENTICATED_KEY, default=lr_s2_authenticated_key ): str, - vol.Optional(CONF_LOG_LEVEL, default=log_level): vol.In( - ADDON_LOG_LEVELS - ), - vol.Optional(CONF_EMULATE_HARDWARE, default=emulate_hardware): bool, } ) diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index 6d5cbb98902..3d626710d52 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -16,8 +16,6 @@ LR_ADDON_VERSION = AwesomeVersion("0.5.0") USER_AGENT = {APPLICATION_NAME: HA_VERSION} CONF_ADDON_DEVICE = "device" -CONF_ADDON_EMULATE_HARDWARE = "emulate_hardware" -CONF_ADDON_LOG_LEVEL = "log_level" CONF_ADDON_NETWORK_KEY = "network_key" CONF_ADDON_S0_LEGACY_KEY = "s0_legacy_key" CONF_ADDON_S2_ACCESS_CONTROL_KEY = "s2_access_control_key" diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json index 439fc7b1aad..d1d4cc94346 100644 --- a/homeassistant/components/zwave_js/strings.json +++ b/homeassistant/components/zwave_js/strings.json @@ -52,8 +52,6 @@ }, "configure_addon_reconfigure": { "data": { - "emulate_hardware": "Emulate Hardware", - "log_level": "Log level", "lr_s2_access_control_key": "[%key:component::zwave_js::config::step::configure_addon_user::data::lr_s2_access_control_key%]", "lr_s2_authenticated_key": "[%key:component::zwave_js::config::step::configure_addon_user::data::lr_s2_authenticated_key%]", "s0_legacy_key": "[%key:component::zwave_js::config::step::configure_addon_user::data::s0_legacy_key%]", diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index fc01c9b29b1..dd8838e0775 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -2566,8 +2566,6 @@ async def test_reconfigure_not_addon_with_addon_stop_fail( "s2_unauthenticated_key": "new987", "lr_s2_access_control_key": "new654", "lr_s2_authenticated_key": "new321", - "log_level": "info", - "emulate_hardware": False, }, 0, ), @@ -2591,8 +2589,6 @@ async def test_reconfigure_not_addon_with_addon_stop_fail( "s2_unauthenticated_key": "new987", "lr_s2_access_control_key": "new654", "lr_s2_authenticated_key": "new321", - "log_level": "info", - "emulate_hardware": False, }, 1, ), @@ -2706,8 +2702,6 @@ async def test_reconfigure_addon_running( "s2_unauthenticated_key": "old987", "lr_s2_access_control_key": "old654", "lr_s2_authenticated_key": "old321", - "log_level": "info", - "emulate_hardware": False, }, { "usb_path": "/test", @@ -2717,8 +2711,6 @@ async def test_reconfigure_addon_running( "s2_unauthenticated_key": "old987", "lr_s2_access_control_key": "old654", "lr_s2_authenticated_key": "old321", - "log_level": "info", - "emulate_hardware": False, }, ), ], @@ -2836,8 +2828,6 @@ async def different_device_server_version(*args): "s2_unauthenticated_key": "old987", "lr_s2_access_control_key": "old654", "lr_s2_authenticated_key": "old321", - "log_level": "info", - "emulate_hardware": False, }, { "usb_path": "/new", @@ -2847,35 +2837,6 @@ async def different_device_server_version(*args): "s2_unauthenticated_key": "new987", "lr_s2_access_control_key": "new654", "lr_s2_authenticated_key": "new321", - "log_level": "info", - "emulate_hardware": False, - }, - 0, - different_device_server_version, - ), - ( - {}, - { - "device": "/test", - "network_key": "old123", - "s0_legacy_key": "old123", - "s2_access_control_key": "old456", - "s2_authenticated_key": "old789", - "s2_unauthenticated_key": "old987", - "lr_s2_access_control_key": "old654", - "lr_s2_authenticated_key": "old321", - "log_level": "info", - }, - { - "usb_path": "/new", - "s0_legacy_key": "new123", - "s2_access_control_key": "new456", - "s2_authenticated_key": "new789", - "s2_unauthenticated_key": "new987", - "lr_s2_access_control_key": "new654", - "lr_s2_authenticated_key": "new321", - "log_level": "info", - "emulate_hardware": False, }, 0, different_device_server_version, @@ -2946,8 +2907,7 @@ async def test_reconfigure_different_device( result = await hass.config_entries.flow.async_configure(result["flow_id"]) await hass.async_block_till_done() - # Default emulate_hardware is False. - addon_options = {"emulate_hardware": False} | old_addon_options + addon_options = {} | old_addon_options # Legacy network key is not reset. addon_options.pop("network_key") @@ -2994,8 +2954,6 @@ async def test_reconfigure_different_device( "s2_unauthenticated_key": "old987", "lr_s2_access_control_key": "old654", "lr_s2_authenticated_key": "old321", - "log_level": "info", - "emulate_hardware": False, }, { "usb_path": "/new", @@ -3005,8 +2963,6 @@ async def test_reconfigure_different_device( "s2_unauthenticated_key": "new987", "lr_s2_access_control_key": "new654", "lr_s2_authenticated_key": "new321", - "log_level": "info", - "emulate_hardware": False, }, 0, [SupervisorError(), None], @@ -3022,8 +2978,6 @@ async def test_reconfigure_different_device( "s2_unauthenticated_key": "old987", "lr_s2_access_control_key": "old654", "lr_s2_authenticated_key": "old321", - "log_level": "info", - "emulate_hardware": False, }, { "usb_path": "/new", @@ -3033,8 +2987,6 @@ async def test_reconfigure_different_device( "s2_unauthenticated_key": "new987", "lr_s2_access_control_key": "new654", "lr_s2_authenticated_key": "new321", - "log_level": "info", - "emulate_hardware": False, }, 0, [ @@ -3151,8 +3103,6 @@ async def test_reconfigure_addon_running_server_info_failure( "s2_unauthenticated_key": "old987", "lr_s2_access_control_key": "old654", "lr_s2_authenticated_key": "old321", - "log_level": "info", - "emulate_hardware": False, } new_addon_options = { "usb_path": "/test", @@ -3162,8 +3112,6 @@ async def test_reconfigure_addon_running_server_info_failure( "s2_unauthenticated_key": "old987", "lr_s2_access_control_key": "old654", "lr_s2_authenticated_key": "old321", - "log_level": "info", - "emulate_hardware": False, } addon_options.update(old_addon_options) entry = integration @@ -3236,8 +3184,6 @@ async def test_reconfigure_addon_running_server_info_failure( "s2_unauthenticated_key": "new987", "lr_s2_access_control_key": "new654", "lr_s2_authenticated_key": "new321", - "log_level": "info", - "emulate_hardware": False, }, 0, ), @@ -3261,8 +3207,6 @@ async def test_reconfigure_addon_running_server_info_failure( "s2_unauthenticated_key": "new987", "lr_s2_access_control_key": "new654", "lr_s2_authenticated_key": "new321", - "log_level": "info", - "emulate_hardware": False, }, 1, ), From 5ee39df3305ee9ecceb1d43e2e444182c98d6a1a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 11 Jun 2025 15:16:51 +0200 Subject: [PATCH 180/266] Handle changes to source entity in history_stats helper (#146521) --- .../components/history_stats/__init__.py | 26 ++ .../components/history_stats/config_flow.py | 2 +- tests/components/history_stats/test_init.py | 294 +++++++++++++++++- 3 files changed, 314 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/history_stats/__init__.py b/homeassistant/components/history_stats/__init__.py index 63f32138dba..a3565f9ed77 100644 --- a/homeassistant/components/history_stats/__init__.py +++ b/homeassistant/components/history_stats/__init__.py @@ -8,8 +8,10 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ENTITY_ID, CONF_STATE from homeassistant.core import HomeAssistant from homeassistant.helpers.device import ( + async_entity_id_to_device_id, async_remove_stale_devices_links_keep_entity_device, ) +from homeassistant.helpers.helper_integration import async_handle_source_entity_changes from homeassistant.helpers.template import Template from .const import CONF_DURATION, CONF_END, CONF_START, PLATFORMS @@ -51,6 +53,30 @@ async def async_setup_entry( entry.options[CONF_ENTITY_ID], ) + def set_source_entity_id_or_uuid(source_entity_id: str) -> None: + hass.config_entries.async_update_entry( + entry, + options={**entry.options, CONF_ENTITY_ID: source_entity_id}, + ) + + async def source_entity_removed() -> None: + # The source entity has been removed, we remove the config entry because + # history_stats does not allow replacing the input entity. + await hass.config_entries.async_remove(entry.entry_id) + + entry.async_on_unload( + async_handle_source_entity_changes( + hass, + helper_config_entry_id=entry.entry_id, + set_source_entity_id_or_uuid=set_source_entity_id_or_uuid, + source_device_id=async_entity_id_to_device_id( + hass, entry.options[CONF_ENTITY_ID] + ), + source_entity_id_or_uuid=entry.options[CONF_ENTITY_ID], + source_entity_removed=source_entity_removed, + ) + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(update_listener)) diff --git a/homeassistant/components/history_stats/config_flow.py b/homeassistant/components/history_stats/config_flow.py index 96c8f319fbc..ca3d5229b6b 100644 --- a/homeassistant/components/history_stats/config_flow.py +++ b/homeassistant/components/history_stats/config_flow.py @@ -107,7 +107,7 @@ OPTIONS_FLOW = { } -class StatisticsConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): +class HistoryStatsConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): """Handle a config flow for History stats.""" config_flow = CONFIG_FLOW diff --git a/tests/components/history_stats/test_init.py b/tests/components/history_stats/test_init.py index 4cd999ba31c..37b5416fdbb 100644 --- a/tests/components/history_stats/test_init.py +++ b/tests/components/history_stats/test_init.py @@ -2,24 +2,107 @@ from __future__ import annotations +from unittest.mock import patch + +import pytest + +from homeassistant.components import history_stats +from homeassistant.components.history_stats.config_flow import ( + HistoryStatsConfigFlowHandler, +) from homeassistant.components.history_stats.const import ( CONF_END, CONF_START, DEFAULT_NAME, DOMAIN as HISTORY_STATS_DOMAIN, ) -from homeassistant.components.recorder import Recorder -from homeassistant.config_entries import ConfigEntryState +from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import CONF_ENTITY_ID, CONF_NAME, CONF_STATE, CONF_TYPE -from homeassistant.core import HomeAssistant +from homeassistant.core import Event, HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.event import async_track_entity_registry_updated_event from tests.common import MockConfigEntry -async def test_unload_entry( - recorder_mock: Recorder, hass: HomeAssistant, loaded_entry: MockConfigEntry -) -> None: +@pytest.fixture +def sensor_config_entry(hass: HomeAssistant) -> er.RegistryEntry: + """Fixture to create a sensor config entry.""" + sensor_config_entry = MockConfigEntry() + sensor_config_entry.add_to_hass(hass) + return sensor_config_entry + + +@pytest.fixture +def sensor_device( + device_registry: dr.DeviceRegistry, sensor_config_entry: ConfigEntry +) -> dr.DeviceEntry: + """Fixture to create a sensor device.""" + return device_registry.async_get_or_create( + config_entry_id=sensor_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + + +@pytest.fixture +def sensor_entity_entry( + entity_registry: er.EntityRegistry, + sensor_config_entry: ConfigEntry, + sensor_device: dr.DeviceEntry, +) -> er.RegistryEntry: + """Fixture to create a sensor entity entry.""" + return entity_registry.async_get_or_create( + "sensor", + "test", + "unique", + config_entry=sensor_config_entry, + device_id=sensor_device.id, + original_name="ABC", + ) + + +@pytest.fixture +def history_stats_config_entry( + hass: HomeAssistant, + sensor_entity_entry: er.RegistryEntry, +) -> MockConfigEntry: + """Fixture to create a history_stats config entry.""" + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + CONF_NAME: DEFAULT_NAME, + CONF_ENTITY_ID: sensor_entity_entry.entity_id, + CONF_STATE: ["on"], + CONF_TYPE: "count", + CONF_START: "{{ as_timestamp(utcnow()) - 3600 }}", + CONF_END: "{{ utcnow() }}", + }, + title="My history stats", + version=HistoryStatsConfigFlowHandler.VERSION, + minor_version=HistoryStatsConfigFlowHandler.MINOR_VERSION, + ) + + config_entry.add_to_hass(hass) + + return config_entry + + +def track_entity_registry_actions(hass: HomeAssistant, entity_id: str) -> list[str]: + """Track entity registry actions for an entity.""" + events = [] + + def add_event(event: Event[er.EventEntityRegistryUpdatedData]) -> None: + """Add entity registry updated event to the list.""" + events.append(event.data["action"]) + + async_track_entity_registry_updated_event(hass, entity_id, add_event) + + return events + + +@pytest.mark.usefixtures("recorder_mock") +async def test_unload_entry(hass: HomeAssistant, loaded_entry: MockConfigEntry) -> None: """Test unload an entry.""" assert loaded_entry.state is ConfigEntryState.LOADED @@ -28,8 +111,8 @@ async def test_unload_entry( assert loaded_entry.state is ConfigEntryState.NOT_LOADED +@pytest.mark.usefixtures("recorder_mock") async def test_device_cleaning( - recorder_mock: Recorder, hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, @@ -116,3 +199,200 @@ async def test_device_cleaning( assert len(devices_after_reload) == 1 assert devices_after_reload[0].id == source_device1_entry.id + + +@pytest.mark.usefixtures("recorder_mock") +async def test_async_handle_source_entity_changes_source_entity_removed( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + history_stats_config_entry: MockConfigEntry, + sensor_config_entry: ConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, +) -> None: + """Test the history_stats config entry is removed when the source entity is removed.""" + # Add another config entry to the sensor device + other_config_entry = MockConfigEntry() + other_config_entry.add_to_hass(hass) + device_registry.async_update_device( + sensor_device.id, add_config_entry_id=other_config_entry.entry_id + ) + + assert await hass.config_entries.async_setup(history_stats_config_entry.entry_id) + await hass.async_block_till_done() + + history_stats_entity_entry = entity_registry.async_get("sensor.my_history_stats") + assert history_stats_entity_entry.device_id == sensor_entity_entry.device_id + + sensor_device = device_registry.async_get(sensor_device.id) + assert history_stats_config_entry.entry_id in sensor_device.config_entries + + events = track_entity_registry_actions(hass, history_stats_entity_entry.entity_id) + + # Remove the source sensor's config entry from the device, this removes the + # source sensor + with patch( + "homeassistant.components.history_stats.async_unload_entry", + wraps=history_stats.async_unload_entry, + ) as mock_unload_entry: + device_registry.async_update_device( + sensor_device.id, remove_config_entry_id=sensor_config_entry.entry_id + ) + await hass.async_block_till_done() + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the history_stats config entry is removed from the device + sensor_device = device_registry.async_get(sensor_device.id) + assert history_stats_config_entry.entry_id not in sensor_device.config_entries + + # Check that the history_stats config entry is removed + assert ( + history_stats_config_entry.entry_id not in hass.config_entries.async_entry_ids() + ) + + # Check we got the expected events + assert events == ["remove"] + + +@pytest.mark.usefixtures("recorder_mock") +async def test_async_handle_source_entity_changes_source_entity_removed_from_device( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + history_stats_config_entry: MockConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, +) -> None: + """Test the source entity removed from the source device.""" + assert await hass.config_entries.async_setup(history_stats_config_entry.entry_id) + await hass.async_block_till_done() + + history_stats_entity_entry = entity_registry.async_get("sensor.my_history_stats") + assert history_stats_entity_entry.device_id == sensor_entity_entry.device_id + + sensor_device = device_registry.async_get(sensor_device.id) + assert history_stats_config_entry.entry_id in sensor_device.config_entries + + events = track_entity_registry_actions(hass, history_stats_entity_entry.entity_id) + + # Remove the source sensor from the device + with patch( + "homeassistant.components.history_stats.async_unload_entry", + wraps=history_stats.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + sensor_entity_entry.entity_id, device_id=None + ) + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the history_stats config entry is removed from the device + sensor_device = device_registry.async_get(sensor_device.id) + assert history_stats_config_entry.entry_id not in sensor_device.config_entries + + # Check that the history_stats config entry is not removed + assert history_stats_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == ["update"] + + +@pytest.mark.usefixtures("recorder_mock") +async def test_async_handle_source_entity_changes_source_entity_moved_other_device( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + history_stats_config_entry: MockConfigEntry, + sensor_config_entry: ConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, +) -> None: + """Test the source entity is moved to another device.""" + sensor_device_2 = device_registry.async_get_or_create( + config_entry_id=sensor_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:FF")}, + ) + + assert await hass.config_entries.async_setup(history_stats_config_entry.entry_id) + await hass.async_block_till_done() + + history_stats_entity_entry = entity_registry.async_get("sensor.my_history_stats") + assert history_stats_entity_entry.device_id == sensor_entity_entry.device_id + + sensor_device = device_registry.async_get(sensor_device.id) + assert history_stats_config_entry.entry_id in sensor_device.config_entries + sensor_device_2 = device_registry.async_get(sensor_device_2.id) + assert history_stats_config_entry.entry_id not in sensor_device_2.config_entries + + events = track_entity_registry_actions(hass, history_stats_entity_entry.entity_id) + + # Move the source sensor to another device + with patch( + "homeassistant.components.history_stats.async_unload_entry", + wraps=history_stats.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + sensor_entity_entry.entity_id, device_id=sensor_device_2.id + ) + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the history_stats config entry is moved to the other device + sensor_device = device_registry.async_get(sensor_device.id) + assert history_stats_config_entry.entry_id not in sensor_device.config_entries + sensor_device_2 = device_registry.async_get(sensor_device_2.id) + assert history_stats_config_entry.entry_id in sensor_device_2.config_entries + + # Check that the history_stats config entry is not removed + assert history_stats_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == ["update"] + + +@pytest.mark.usefixtures("recorder_mock") +async def test_async_handle_source_entity_new_entity_id( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + history_stats_config_entry: MockConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, +) -> None: + """Test the source entity's entity ID is changed.""" + assert await hass.config_entries.async_setup(history_stats_config_entry.entry_id) + await hass.async_block_till_done() + + history_stats_entity_entry = entity_registry.async_get("sensor.my_history_stats") + assert history_stats_entity_entry.device_id == sensor_entity_entry.device_id + + sensor_device = device_registry.async_get(sensor_device.id) + assert history_stats_config_entry.entry_id in sensor_device.config_entries + + events = track_entity_registry_actions(hass, history_stats_entity_entry.entity_id) + + # Change the source entity's entity ID + with patch( + "homeassistant.components.history_stats.async_unload_entry", + wraps=history_stats.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + sensor_entity_entry.entity_id, new_entity_id="sensor.new_entity_id" + ) + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the history_stats config entry is updated with the new entity ID + assert history_stats_config_entry.options[CONF_ENTITY_ID] == "sensor.new_entity_id" + + # Check that the helper config is still in the device + sensor_device = device_registry.async_get(sensor_device.id) + assert history_stats_config_entry.entry_id in sensor_device.config_entries + + # Check that the history_stats config entry is not removed + assert history_stats_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == [] From 0cf1fd1d41bee8170e57d5b201d7299103cda910 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 11 Jun 2025 15:17:08 +0200 Subject: [PATCH 181/266] Handle changes to source entity in integration helper (#146522) --- .../components/integration/__init__.py | 25 ++ tests/components/integration/test_init.py | 276 +++++++++++++++++- 2 files changed, 300 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/integration/__init__.py b/homeassistant/components/integration/__init__.py index 4ccf0dec258..0a64ce7140f 100644 --- a/homeassistant/components/integration/__init__.py +++ b/homeassistant/components/integration/__init__.py @@ -6,8 +6,10 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.device import ( + async_entity_id_to_device_id, async_remove_stale_devices_links_keep_entity_device, ) +from homeassistant.helpers.helper_integration import async_handle_source_entity_changes from .const import CONF_SOURCE_SENSOR @@ -21,6 +23,29 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.options[CONF_SOURCE_SENSOR], ) + def set_source_entity_id_or_uuid(source_entity_id: str) -> None: + hass.config_entries.async_update_entry( + entry, + options={**entry.options, CONF_SOURCE_SENSOR: source_entity_id}, + ) + + async def source_entity_removed() -> None: + # The source entity has been removed, we need to clean the device links. + async_remove_stale_devices_links_keep_entity_device(hass, entry.entry_id, None) + + entry.async_on_unload( + async_handle_source_entity_changes( + hass, + helper_config_entry_id=entry.entry_id, + set_source_entity_id_or_uuid=set_source_entity_id_or_uuid, + source_device_id=async_entity_id_to_device_id( + hass, entry.options[CONF_SOURCE_SENSOR] + ), + source_entity_id_or_uuid=entry.options[CONF_SOURCE_SENSOR], + source_entity_removed=source_entity_removed, + ) + ) + await hass.config_entries.async_forward_entry_setups(entry, (Platform.SENSOR,)) entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) return True diff --git a/tests/components/integration/test_init.py b/tests/components/integration/test_init.py index 9fee54f4500..0ce3297a2ff 100644 --- a/tests/components/integration/test_init.py +++ b/tests/components/integration/test_init.py @@ -1,14 +1,97 @@ """Test the Integration - Riemann sum integral integration.""" +from unittest.mock import patch + import pytest +from homeassistant.components import integration +from homeassistant.components.integration.config_flow import ConfigFlowHandler from homeassistant.components.integration.const import DOMAIN -from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import Event, HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.event import async_track_entity_registry_updated_event from tests.common import MockConfigEntry +@pytest.fixture +def sensor_config_entry(hass: HomeAssistant) -> er.RegistryEntry: + """Fixture to create a sensor config entry.""" + sensor_config_entry = MockConfigEntry() + sensor_config_entry.add_to_hass(hass) + return sensor_config_entry + + +@pytest.fixture +def sensor_device( + device_registry: dr.DeviceRegistry, sensor_config_entry: ConfigEntry +) -> dr.DeviceEntry: + """Fixture to create a sensor device.""" + return device_registry.async_get_or_create( + config_entry_id=sensor_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + + +@pytest.fixture +def sensor_entity_entry( + entity_registry: er.EntityRegistry, + sensor_config_entry: ConfigEntry, + sensor_device: dr.DeviceEntry, +) -> er.RegistryEntry: + """Fixture to create a sensor entity entry.""" + return entity_registry.async_get_or_create( + "sensor", + "test", + "unique", + config_entry=sensor_config_entry, + device_id=sensor_device.id, + original_name="ABC", + ) + + +@pytest.fixture +def integration_config_entry( + hass: HomeAssistant, + sensor_entity_entry: er.RegistryEntry, +) -> MockConfigEntry: + """Fixture to create an integration config entry.""" + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "method": "trapezoidal", + "name": "My integration", + "round": 1.0, + "source": sensor_entity_entry.entity_id, + "unit_prefix": "k", + "unit_time": "min", + "max_sub_interval": {"minutes": 1}, + }, + title="My integration", + version=ConfigFlowHandler.VERSION, + minor_version=ConfigFlowHandler.MINOR_VERSION, + ) + + config_entry.add_to_hass(hass) + + return config_entry + + +def track_entity_registry_actions(hass: HomeAssistant, entity_id: str) -> list[str]: + """Track entity registry actions for an entity.""" + events = [] + + def add_event(event: Event[er.EventEntityRegistryUpdatedData]) -> None: + """Add entity registry updated event to the list.""" + events.append(event.data["action"]) + + async_track_entity_registry_updated_event(hass, entity_id, add_event) + + return events + + @pytest.mark.parametrize("platform", ["sensor"]) async def test_setup_and_remove_config_entry( hass: HomeAssistant, @@ -209,3 +292,194 @@ async def test_device_cleaning( integration_config_entry.entry_id ) assert len(devices_after_reload) == 1 + + +async def test_async_handle_source_entity_changes_source_entity_removed( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + integration_config_entry: MockConfigEntry, + sensor_config_entry: ConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, +) -> None: + """Test the integration config entry is removed when the source entity is removed.""" + # Add another config entry to the sensor device + other_config_entry = MockConfigEntry() + other_config_entry.add_to_hass(hass) + device_registry.async_update_device( + sensor_device.id, add_config_entry_id=other_config_entry.entry_id + ) + + assert await hass.config_entries.async_setup(integration_config_entry.entry_id) + await hass.async_block_till_done() + + integration_entity_entry = entity_registry.async_get("sensor.my_integration") + assert integration_entity_entry.device_id == sensor_entity_entry.device_id + + sensor_device = device_registry.async_get(sensor_device.id) + assert integration_config_entry.entry_id in sensor_device.config_entries + + events = track_entity_registry_actions(hass, integration_entity_entry.entity_id) + + # Remove the source sensor's config entry from the device, this removes the + # source sensor + with patch( + "homeassistant.components.integration.async_unload_entry", + wraps=integration.async_unload_entry, + ) as mock_unload_entry: + device_registry.async_update_device( + sensor_device.id, remove_config_entry_id=sensor_config_entry.entry_id + ) + await hass.async_block_till_done() + await hass.async_block_till_done() + mock_unload_entry.assert_not_called() + + # Check that the integration config entry is removed from the device + sensor_device = device_registry.async_get(sensor_device.id) + assert integration_config_entry.entry_id not in sensor_device.config_entries + + # Check that the integration config entry is not removed + assert integration_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == ["update"] + + +async def test_async_handle_source_entity_changes_source_entity_removed_from_device( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + integration_config_entry: MockConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, +) -> None: + """Test the source entity removed from the source device.""" + assert await hass.config_entries.async_setup(integration_config_entry.entry_id) + await hass.async_block_till_done() + + integration_entity_entry = entity_registry.async_get("sensor.my_integration") + assert integration_entity_entry.device_id == sensor_entity_entry.device_id + + sensor_device = device_registry.async_get(sensor_device.id) + assert integration_config_entry.entry_id in sensor_device.config_entries + + events = track_entity_registry_actions(hass, integration_entity_entry.entity_id) + + # Remove the source sensor from the device + with patch( + "homeassistant.components.integration.async_unload_entry", + wraps=integration.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + sensor_entity_entry.entity_id, device_id=None + ) + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the integration config entry is removed from the device + sensor_device = device_registry.async_get(sensor_device.id) + assert integration_config_entry.entry_id not in sensor_device.config_entries + + # Check that the integration config entry is not removed + assert integration_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == ["update"] + + +async def test_async_handle_source_entity_changes_source_entity_moved_other_device( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + integration_config_entry: MockConfigEntry, + sensor_config_entry: ConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, +) -> None: + """Test the source entity is moved to another device.""" + sensor_device_2 = device_registry.async_get_or_create( + config_entry_id=sensor_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:FF")}, + ) + + assert await hass.config_entries.async_setup(integration_config_entry.entry_id) + await hass.async_block_till_done() + + integration_entity_entry = entity_registry.async_get("sensor.my_integration") + assert integration_entity_entry.device_id == sensor_entity_entry.device_id + + sensor_device = device_registry.async_get(sensor_device.id) + assert integration_config_entry.entry_id in sensor_device.config_entries + sensor_device_2 = device_registry.async_get(sensor_device_2.id) + assert integration_config_entry.entry_id not in sensor_device_2.config_entries + + events = track_entity_registry_actions(hass, integration_entity_entry.entity_id) + + # Move the source sensor to another device + with patch( + "homeassistant.components.integration.async_unload_entry", + wraps=integration.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + sensor_entity_entry.entity_id, device_id=sensor_device_2.id + ) + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the integration config entry is moved to the other device + sensor_device = device_registry.async_get(sensor_device.id) + assert integration_config_entry.entry_id not in sensor_device.config_entries + sensor_device_2 = device_registry.async_get(sensor_device_2.id) + assert integration_config_entry.entry_id in sensor_device_2.config_entries + + # Check that the integration config entry is not removed + assert integration_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == ["update"] + + +async def test_async_handle_source_entity_new_entity_id( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + integration_config_entry: MockConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, +) -> None: + """Test the source entity's entity ID is changed.""" + assert await hass.config_entries.async_setup(integration_config_entry.entry_id) + await hass.async_block_till_done() + + integration_entity_entry = entity_registry.async_get("sensor.my_integration") + assert integration_entity_entry.device_id == sensor_entity_entry.device_id + + sensor_device = device_registry.async_get(sensor_device.id) + assert integration_config_entry.entry_id in sensor_device.config_entries + + events = track_entity_registry_actions(hass, integration_entity_entry.entity_id) + + # Change the source entity's entity ID + with patch( + "homeassistant.components.integration.async_unload_entry", + wraps=integration.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + sensor_entity_entry.entity_id, new_entity_id="sensor.new_entity_id" + ) + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the integration config entry is updated with the new entity ID + assert integration_config_entry.options["source"] == "sensor.new_entity_id" + + # Check that the helper config is still in the device + sensor_device = device_registry.async_get(sensor_device.id) + assert integration_config_entry.entry_id in sensor_device.config_entries + + # Check that the integration config entry is not removed + assert integration_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == [] From caaa4d5f3516038a9bcd6dc87d959e403e1640f1 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 11 Jun 2025 15:17:34 +0200 Subject: [PATCH 182/266] Handle changes to source entity in threshold helper (#146524) --- .../components/threshold/__init__.py | 25 ++ tests/components/threshold/test_init.py | 274 +++++++++++++++++- 2 files changed, 298 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/threshold/__init__.py b/homeassistant/components/threshold/__init__.py index ea8b469fd32..9460a50db80 100644 --- a/homeassistant/components/threshold/__init__.py +++ b/homeassistant/components/threshold/__init__.py @@ -4,8 +4,10 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ENTITY_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.device import ( + async_entity_id_to_device_id, async_remove_stale_devices_links_keep_entity_device, ) +from homeassistant.helpers.helper_integration import async_handle_source_entity_changes async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -17,6 +19,29 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.options[CONF_ENTITY_ID], ) + def set_source_entity_id_or_uuid(source_entity_id: str) -> None: + hass.config_entries.async_update_entry( + entry, + options={**entry.options, CONF_ENTITY_ID: source_entity_id}, + ) + + async def source_entity_removed() -> None: + # The source entity has been removed, we need to clean the device links. + async_remove_stale_devices_links_keep_entity_device(hass, entry.entry_id, None) + + entry.async_on_unload( + async_handle_source_entity_changes( + hass, + helper_config_entry_id=entry.entry_id, + set_source_entity_id_or_uuid=set_source_entity_id_or_uuid, + source_device_id=async_entity_id_to_device_id( + hass, entry.options[CONF_ENTITY_ID] + ), + source_entity_id_or_uuid=entry.options[CONF_ENTITY_ID], + source_entity_removed=source_entity_removed, + ) + ) + await hass.config_entries.async_forward_entry_setups( entry, (Platform.BINARY_SENSOR,) ) diff --git a/tests/components/threshold/test_init.py b/tests/components/threshold/test_init.py index 6e85d659922..599612ce0b7 100644 --- a/tests/components/threshold/test_init.py +++ b/tests/components/threshold/test_init.py @@ -1,14 +1,95 @@ """Test the Min/Max integration.""" +from unittest.mock import patch + import pytest +from homeassistant.components import threshold +from homeassistant.components.threshold.config_flow import ConfigFlowHandler from homeassistant.components.threshold.const import DOMAIN -from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import Event, HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.event import async_track_entity_registry_updated_event from tests.common import MockConfigEntry +@pytest.fixture +def sensor_config_entry(hass: HomeAssistant) -> er.RegistryEntry: + """Fixture to create a sensor config entry.""" + sensor_config_entry = MockConfigEntry() + sensor_config_entry.add_to_hass(hass) + return sensor_config_entry + + +@pytest.fixture +def sensor_device( + device_registry: dr.DeviceRegistry, sensor_config_entry: ConfigEntry +) -> dr.DeviceEntry: + """Fixture to create a sensor device.""" + return device_registry.async_get_or_create( + config_entry_id=sensor_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + + +@pytest.fixture +def sensor_entity_entry( + entity_registry: er.EntityRegistry, + sensor_config_entry: ConfigEntry, + sensor_device: dr.DeviceEntry, +) -> er.RegistryEntry: + """Fixture to create a sensor entity entry.""" + return entity_registry.async_get_or_create( + "sensor", + "test", + "unique", + config_entry=sensor_config_entry, + device_id=sensor_device.id, + original_name="ABC", + ) + + +@pytest.fixture +def threshold_config_entry( + hass: HomeAssistant, + sensor_entity_entry: er.RegistryEntry, +) -> MockConfigEntry: + """Fixture to create a threshold config entry.""" + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "entity_id": sensor_entity_entry.entity_id, + "hysteresis": 0.0, + "lower": -2.0, + "name": "My threshold", + "upper": None, + }, + title="My threshold", + version=ConfigFlowHandler.VERSION, + minor_version=ConfigFlowHandler.MINOR_VERSION, + ) + + config_entry.add_to_hass(hass) + + return config_entry + + +def track_entity_registry_actions(hass: HomeAssistant, entity_id: str) -> list[str]: + """Track entity registry actions for an entity.""" + events = [] + + def add_event(event: Event[er.EventEntityRegistryUpdatedData]) -> None: + """Add entity registry updated event to the list.""" + events.append(event.data["action"]) + + async_track_entity_registry_updated_event(hass, entity_id, add_event) + + return events + + @pytest.mark.parametrize("platform", ["binary_sensor"]) async def test_setup_and_remove_config_entry( hass: HomeAssistant, @@ -208,3 +289,194 @@ async def test_device_cleaning( threshold_config_entry.entry_id ) assert len(devices_after_reload) == 1 + + +async def test_async_handle_source_entity_changes_source_entity_removed( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + threshold_config_entry: MockConfigEntry, + sensor_config_entry: ConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, +) -> None: + """Test the threshold config entry is removed when the source entity is removed.""" + # Add another config entry to the sensor device + other_config_entry = MockConfigEntry() + other_config_entry.add_to_hass(hass) + device_registry.async_update_device( + sensor_device.id, add_config_entry_id=other_config_entry.entry_id + ) + + assert await hass.config_entries.async_setup(threshold_config_entry.entry_id) + await hass.async_block_till_done() + + threshold_entity_entry = entity_registry.async_get("binary_sensor.my_threshold") + assert threshold_entity_entry.device_id == sensor_entity_entry.device_id + + sensor_device = device_registry.async_get(sensor_device.id) + assert threshold_config_entry.entry_id in sensor_device.config_entries + + events = track_entity_registry_actions(hass, threshold_entity_entry.entity_id) + + # Remove the source sensor's config entry from the device, this removes the + # source sensor + with patch( + "homeassistant.components.threshold.async_unload_entry", + wraps=threshold.async_unload_entry, + ) as mock_unload_entry: + device_registry.async_update_device( + sensor_device.id, remove_config_entry_id=sensor_config_entry.entry_id + ) + await hass.async_block_till_done() + await hass.async_block_till_done() + mock_unload_entry.assert_not_called() + + # Check that the threshold config entry is removed from the device + sensor_device = device_registry.async_get(sensor_device.id) + assert threshold_config_entry.entry_id not in sensor_device.config_entries + + # Check that the threshold config entry is not removed + assert threshold_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == ["update"] + + +async def test_async_handle_source_entity_changes_source_entity_removed_from_device( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + threshold_config_entry: MockConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, +) -> None: + """Test the source entity removed from the source device.""" + assert await hass.config_entries.async_setup(threshold_config_entry.entry_id) + await hass.async_block_till_done() + + threshold_entity_entry = entity_registry.async_get("binary_sensor.my_threshold") + assert threshold_entity_entry.device_id == sensor_entity_entry.device_id + + sensor_device = device_registry.async_get(sensor_device.id) + assert threshold_config_entry.entry_id in sensor_device.config_entries + + events = track_entity_registry_actions(hass, threshold_entity_entry.entity_id) + + # Remove the source sensor from the device + with patch( + "homeassistant.components.threshold.async_unload_entry", + wraps=threshold.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + sensor_entity_entry.entity_id, device_id=None + ) + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the threshold config entry is removed from the device + sensor_device = device_registry.async_get(sensor_device.id) + assert threshold_config_entry.entry_id not in sensor_device.config_entries + + # Check that the threshold config entry is not removed + assert threshold_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == ["update"] + + +async def test_async_handle_source_entity_changes_source_entity_moved_other_device( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + threshold_config_entry: MockConfigEntry, + sensor_config_entry: ConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, +) -> None: + """Test the source entity is moved to another device.""" + sensor_device_2 = device_registry.async_get_or_create( + config_entry_id=sensor_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:FF")}, + ) + + assert await hass.config_entries.async_setup(threshold_config_entry.entry_id) + await hass.async_block_till_done() + + threshold_entity_entry = entity_registry.async_get("binary_sensor.my_threshold") + assert threshold_entity_entry.device_id == sensor_entity_entry.device_id + + sensor_device = device_registry.async_get(sensor_device.id) + assert threshold_config_entry.entry_id in sensor_device.config_entries + sensor_device_2 = device_registry.async_get(sensor_device_2.id) + assert threshold_config_entry.entry_id not in sensor_device_2.config_entries + + events = track_entity_registry_actions(hass, threshold_entity_entry.entity_id) + + # Move the source sensor to another device + with patch( + "homeassistant.components.threshold.async_unload_entry", + wraps=threshold.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + sensor_entity_entry.entity_id, device_id=sensor_device_2.id + ) + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the threshold config entry is moved to the other device + sensor_device = device_registry.async_get(sensor_device.id) + assert threshold_config_entry.entry_id not in sensor_device.config_entries + sensor_device_2 = device_registry.async_get(sensor_device_2.id) + assert threshold_config_entry.entry_id in sensor_device_2.config_entries + + # Check that the threshold config entry is not removed + assert threshold_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == ["update"] + + +async def test_async_handle_source_entity_new_entity_id( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + threshold_config_entry: MockConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, +) -> None: + """Test the source entity's entity ID is changed.""" + assert await hass.config_entries.async_setup(threshold_config_entry.entry_id) + await hass.async_block_till_done() + + threshold_entity_entry = entity_registry.async_get("binary_sensor.my_threshold") + assert threshold_entity_entry.device_id == sensor_entity_entry.device_id + + sensor_device = device_registry.async_get(sensor_device.id) + assert threshold_config_entry.entry_id in sensor_device.config_entries + + events = track_entity_registry_actions(hass, threshold_entity_entry.entity_id) + + # Change the source entity's entity ID + with patch( + "homeassistant.components.threshold.async_unload_entry", + wraps=threshold.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + sensor_entity_entry.entity_id, new_entity_id="sensor.new_entity_id" + ) + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the threshold config entry is updated with the new entity ID + assert threshold_config_entry.options["entity_id"] == "sensor.new_entity_id" + + # Check that the helper config is still in the device + sensor_device = device_registry.async_get(sensor_device.id) + assert threshold_config_entry.entry_id in sensor_device.config_entries + + # Check that the threshold config entry is not removed + assert threshold_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == [] From 273ccb3929c921dbe3f53f15474182852aa96f06 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 11 Jun 2025 15:17:42 +0200 Subject: [PATCH 183/266] Handle changes to source entity in trend helper (#146525) --- homeassistant/components/trend/__init__.py | 26 ++ tests/components/trend/test_init.py | 275 ++++++++++++++++++++- 2 files changed, 299 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/trend/__init__.py b/homeassistant/components/trend/__init__.py index c38730e7591..086ac818c8e 100644 --- a/homeassistant/components/trend/__init__.py +++ b/homeassistant/components/trend/__init__.py @@ -6,8 +6,10 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ENTITY_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.device import ( + async_entity_id_to_device_id, async_remove_stale_devices_links_keep_entity_device, ) +from homeassistant.helpers.helper_integration import async_handle_source_entity_changes PLATFORMS = [Platform.BINARY_SENSOR] @@ -21,6 +23,30 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.options[CONF_ENTITY_ID], ) + def set_source_entity_id_or_uuid(source_entity_id: str) -> None: + hass.config_entries.async_update_entry( + entry, + options={**entry.options, CONF_ENTITY_ID: source_entity_id}, + ) + + async def source_entity_removed() -> None: + # The source entity has been removed, we remove the config entry because + # trend does not allow replacing the input entity. + await hass.config_entries.async_remove(entry.entry_id) + + entry.async_on_unload( + async_handle_source_entity_changes( + hass, + helper_config_entry_id=entry.entry_id, + set_source_entity_id_or_uuid=set_source_entity_id_or_uuid, + source_device_id=async_entity_id_to_device_id( + hass, entry.options[CONF_ENTITY_ID] + ), + source_entity_id_or_uuid=entry.options[CONF_ENTITY_ID], + source_entity_removed=source_entity_removed, + ) + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) diff --git a/tests/components/trend/test_init.py b/tests/components/trend/test_init.py index 7ffb18de297..4ff6213d082 100644 --- a/tests/components/trend/test_init.py +++ b/tests/components/trend/test_init.py @@ -1,15 +1,95 @@ """Test the Trend integration.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components import trend +from homeassistant.components.trend.config_flow import ConfigFlowHandler from homeassistant.components.trend.const import DOMAIN -from homeassistant.config_entries import ConfigEntryState -from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry, ConfigEntryState +from homeassistant.core import Event, HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.event import async_track_entity_registry_updated_event from .conftest import ComponentSetup from tests.common import MockConfigEntry +@pytest.fixture +def sensor_config_entry(hass: HomeAssistant) -> er.RegistryEntry: + """Fixture to create a sensor config entry.""" + sensor_config_entry = MockConfigEntry() + sensor_config_entry.add_to_hass(hass) + return sensor_config_entry + + +@pytest.fixture +def sensor_device( + device_registry: dr.DeviceRegistry, sensor_config_entry: ConfigEntry +) -> dr.DeviceEntry: + """Fixture to create a sensor device.""" + return device_registry.async_get_or_create( + config_entry_id=sensor_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + + +@pytest.fixture +def sensor_entity_entry( + entity_registry: er.EntityRegistry, + sensor_config_entry: ConfigEntry, + sensor_device: dr.DeviceEntry, +) -> er.RegistryEntry: + """Fixture to create a sensor entity entry.""" + return entity_registry.async_get_or_create( + "sensor", + "test", + "unique", + config_entry=sensor_config_entry, + device_id=sensor_device.id, + original_name="ABC", + ) + + +@pytest.fixture +def trend_config_entry( + hass: HomeAssistant, + sensor_entity_entry: er.RegistryEntry, +) -> MockConfigEntry: + """Fixture to create a trend config entry.""" + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "name": "My trend", + "entity_id": sensor_entity_entry.entity_id, + "invert": False, + }, + title="My trend", + version=ConfigFlowHandler.VERSION, + minor_version=ConfigFlowHandler.MINOR_VERSION, + ) + + config_entry.add_to_hass(hass) + + return config_entry + + +def track_entity_registry_actions(hass: HomeAssistant, entity_id: str) -> list[str]: + """Track entity registry actions for an entity.""" + events = [] + + def add_event(event: Event[er.EventEntityRegistryUpdatedData]) -> None: + """Add entity registry updated event to the list.""" + events.append(event.data["action"]) + + async_track_entity_registry_updated_event(hass, entity_id, add_event) + + return events + + async def test_setup_and_remove_config_entry( hass: HomeAssistant, entity_registry: er.EntityRegistry, @@ -135,3 +215,194 @@ async def test_device_cleaning( trend_config_entry.entry_id ) assert len(devices_after_reload) == 1 + + +async def test_async_handle_source_entity_changes_source_entity_removed( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + trend_config_entry: MockConfigEntry, + sensor_config_entry: ConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, +) -> None: + """Test the trend config entry is removed when the source entity is removed.""" + # Add another config entry to the sensor device + other_config_entry = MockConfigEntry() + other_config_entry.add_to_hass(hass) + device_registry.async_update_device( + sensor_device.id, add_config_entry_id=other_config_entry.entry_id + ) + + assert await hass.config_entries.async_setup(trend_config_entry.entry_id) + await hass.async_block_till_done() + + trend_entity_entry = entity_registry.async_get("binary_sensor.my_trend") + assert trend_entity_entry.device_id == sensor_entity_entry.device_id + + sensor_device = device_registry.async_get(sensor_device.id) + assert trend_config_entry.entry_id in sensor_device.config_entries + + events = track_entity_registry_actions(hass, trend_entity_entry.entity_id) + + # Remove the source sensor's config entry from the device, this removes the + # source sensor + with patch( + "homeassistant.components.trend.async_unload_entry", + wraps=trend.async_unload_entry, + ) as mock_unload_entry: + device_registry.async_update_device( + sensor_device.id, remove_config_entry_id=sensor_config_entry.entry_id + ) + await hass.async_block_till_done() + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the trend config entry is removed from the device + sensor_device = device_registry.async_get(sensor_device.id) + assert trend_config_entry.entry_id not in sensor_device.config_entries + + # Check that the trend config entry is removed + assert trend_config_entry.entry_id not in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == ["remove"] + + +async def test_async_handle_source_entity_changes_source_entity_removed_from_device( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + trend_config_entry: MockConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, +) -> None: + """Test the source entity removed from the source device.""" + assert await hass.config_entries.async_setup(trend_config_entry.entry_id) + await hass.async_block_till_done() + + trend_entity_entry = entity_registry.async_get("binary_sensor.my_trend") + assert trend_entity_entry.device_id == sensor_entity_entry.device_id + + sensor_device = device_registry.async_get(sensor_device.id) + assert trend_config_entry.entry_id in sensor_device.config_entries + + events = track_entity_registry_actions(hass, trend_entity_entry.entity_id) + + # Remove the source sensor from the device + with patch( + "homeassistant.components.trend.async_unload_entry", + wraps=trend.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + sensor_entity_entry.entity_id, device_id=None + ) + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the trend config entry is removed from the device + sensor_device = device_registry.async_get(sensor_device.id) + assert trend_config_entry.entry_id not in sensor_device.config_entries + + # Check that the trend config entry is not removed + assert trend_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == ["update"] + + +async def test_async_handle_source_entity_changes_source_entity_moved_other_device( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + trend_config_entry: MockConfigEntry, + sensor_config_entry: ConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, +) -> None: + """Test the source entity is moved to another device.""" + sensor_device_2 = device_registry.async_get_or_create( + config_entry_id=sensor_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:FF")}, + ) + + assert await hass.config_entries.async_setup(trend_config_entry.entry_id) + await hass.async_block_till_done() + + trend_entity_entry = entity_registry.async_get("binary_sensor.my_trend") + assert trend_entity_entry.device_id == sensor_entity_entry.device_id + + sensor_device = device_registry.async_get(sensor_device.id) + assert trend_config_entry.entry_id in sensor_device.config_entries + sensor_device_2 = device_registry.async_get(sensor_device_2.id) + assert trend_config_entry.entry_id not in sensor_device_2.config_entries + + events = track_entity_registry_actions(hass, trend_entity_entry.entity_id) + + # Move the source sensor to another device + with patch( + "homeassistant.components.trend.async_unload_entry", + wraps=trend.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + sensor_entity_entry.entity_id, device_id=sensor_device_2.id + ) + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the trend config entry is moved to the other device + sensor_device = device_registry.async_get(sensor_device.id) + assert trend_config_entry.entry_id not in sensor_device.config_entries + sensor_device_2 = device_registry.async_get(sensor_device_2.id) + assert trend_config_entry.entry_id in sensor_device_2.config_entries + + # Check that the trend config entry is not removed + assert trend_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == ["update"] + + +async def test_async_handle_source_entity_new_entity_id( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + trend_config_entry: MockConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, +) -> None: + """Test the source entity's entity ID is changed.""" + assert await hass.config_entries.async_setup(trend_config_entry.entry_id) + await hass.async_block_till_done() + + trend_entity_entry = entity_registry.async_get("binary_sensor.my_trend") + assert trend_entity_entry.device_id == sensor_entity_entry.device_id + + sensor_device = device_registry.async_get(sensor_device.id) + assert trend_config_entry.entry_id in sensor_device.config_entries + + events = track_entity_registry_actions(hass, trend_entity_entry.entity_id) + + # Change the source entity's entity ID + with patch( + "homeassistant.components.trend.async_unload_entry", + wraps=trend.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + sensor_entity_entry.entity_id, new_entity_id="sensor.new_entity_id" + ) + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the trend config entry is updated with the new entity ID + assert trend_config_entry.options["entity_id"] == "sensor.new_entity_id" + + # Check that the helper config is still in the device + sensor_device = device_registry.async_get(sensor_device.id) + assert trend_config_entry.entry_id in sensor_device.config_entries + + # Check that the trend config entry is not removed + assert trend_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == [] From 2ab32220ed2ad926272f87b34b5a02737760de73 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 11 Jun 2025 15:17:52 +0200 Subject: [PATCH 184/266] Handle changes to source entity in utility_meter (#146526) --- .../components/utility_meter/__init__.py | 25 ++ tests/components/utility_meter/test_init.py | 369 +++++++++++++++++- 2 files changed, 393 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/utility_meter/__init__.py b/homeassistant/components/utility_meter/__init__.py index e2b3411c193..64fa3342c08 100644 --- a/homeassistant/components/utility_meter/__init__.py +++ b/homeassistant/components/utility_meter/__init__.py @@ -17,9 +17,11 @@ from homeassistant.helpers import ( entity_registry as er, ) from homeassistant.helpers.device import ( + async_entity_id_to_device_id, async_remove_stale_devices_links_keep_entity_device, ) from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.helper_integration import async_handle_source_entity_changes from homeassistant.helpers.typing import ConfigType from .const import ( @@ -217,6 +219,29 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) return False + def set_source_entity_id_or_uuid(source_entity_id: str) -> None: + hass.config_entries.async_update_entry( + entry, + options={**entry.options, CONF_SOURCE_SENSOR: source_entity_id}, + ) + + async def source_entity_removed() -> None: + # The source entity has been removed, we need to clean the device links. + async_remove_stale_devices_links_keep_entity_device(hass, entry.entry_id, None) + + entry.async_on_unload( + async_handle_source_entity_changes( + hass, + helper_config_entry_id=entry.entry_id, + set_source_entity_id_or_uuid=set_source_entity_id_or_uuid, + source_device_id=async_entity_id_to_device_id( + hass, entry.options[CONF_SOURCE_SENSOR] + ), + source_entity_id_or_uuid=entry.options[CONF_SOURCE_SENSOR], + source_entity_removed=source_entity_removed, + ) + ) + if not entry.options.get(CONF_TARIFFS): # Only a single meter sensor is required hass.data[DATA_UTILITY][entry.entry_id][CONF_TARIFF_ENTITY] = None diff --git a/tests/components/utility_meter/test_init.py b/tests/components/utility_meter/test_init.py index eba7cf913db..ea4af741e19 100644 --- a/tests/components/utility_meter/test_init.py +++ b/tests/components/utility_meter/test_init.py @@ -3,10 +3,12 @@ from __future__ import annotations from datetime import timedelta +from unittest.mock import patch from freezegun import freeze_time import pytest +from homeassistant.components import utility_meter from homeassistant.components.select import ( ATTR_OPTION, DOMAIN as SELECT_DOMAIN, @@ -16,7 +18,9 @@ from homeassistant.components.utility_meter import ( select as um_select, sensor as um_sensor, ) +from homeassistant.components.utility_meter.config_flow import ConfigFlowHandler from homeassistant.components.utility_meter.const import DOMAIN, SERVICE_RESET +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, @@ -25,14 +29,94 @@ from homeassistant.const import ( Platform, UnitOfEnergy, ) -from homeassistant.core import HomeAssistant, State +from homeassistant.core import Event, HomeAssistant, State from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.event import async_track_entity_registry_updated_event from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util from tests.common import MockConfigEntry, mock_restore_cache +@pytest.fixture +def sensor_config_entry(hass: HomeAssistant) -> er.RegistryEntry: + """Fixture to create a sensor config entry.""" + sensor_config_entry = MockConfigEntry() + sensor_config_entry.add_to_hass(hass) + return sensor_config_entry + + +@pytest.fixture +def sensor_device( + device_registry: dr.DeviceRegistry, sensor_config_entry: ConfigEntry +) -> dr.DeviceEntry: + """Fixture to create a sensor device.""" + return device_registry.async_get_or_create( + config_entry_id=sensor_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + + +@pytest.fixture +def sensor_entity_entry( + entity_registry: er.EntityRegistry, + sensor_config_entry: ConfigEntry, + sensor_device: dr.DeviceEntry, +) -> er.RegistryEntry: + """Fixture to create a sensor entity entry.""" + return entity_registry.async_get_or_create( + "sensor", + "test", + "unique", + config_entry=sensor_config_entry, + device_id=sensor_device.id, + original_name="ABC", + ) + + +@pytest.fixture +def utility_meter_config_entry( + hass: HomeAssistant, + sensor_entity_entry: er.RegistryEntry, + tariffs: list[str], +) -> MockConfigEntry: + """Fixture to create a utility_meter config entry.""" + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "cycle": "monthly", + "delta_values": False, + "name": "My utility meter", + "net_consumption": False, + "offset": 0, + "periodically_resetting": True, + "source": sensor_entity_entry.entity_id, + "tariffs": tariffs, + }, + title="My utility meter", + version=ConfigFlowHandler.VERSION, + minor_version=ConfigFlowHandler.MINOR_VERSION, + ) + + config_entry.add_to_hass(hass) + + return config_entry + + +def track_entity_registry_actions(hass: HomeAssistant, entity_id: str) -> list[str]: + """Track entity registry actions for an entity.""" + events = [] + + def add_event(event: Event[er.EventEntityRegistryUpdatedData]) -> None: + """Add entity registry updated event to the list.""" + events.append(event.data["action"]) + + async_track_entity_registry_updated_event(hass, entity_id, add_event) + + return events + + async def test_restore_state(hass: HomeAssistant) -> None: """Test utility sensor restore state.""" config = { @@ -533,3 +617,286 @@ async def test_device_cleaning( utility_meter_config_entry.entry_id ) assert len(devices_after_reload) == 1 + + +@pytest.mark.parametrize( + ("tariffs", "expected_entities"), + [ + ([], {"sensor.my_utility_meter"}), + ( + ["peak", "offpeak"], + { + "select.my_utility_meter", + "sensor.my_utility_meter_offpeak", + "sensor.my_utility_meter_peak", + }, + ), + ], +) +async def test_async_handle_source_entity_changes_source_entity_removed( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + utility_meter_config_entry: MockConfigEntry, + sensor_config_entry: ConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, + expected_entities: set[str], +) -> None: + """Test the utility_meter config entry is removed when the source entity is removed.""" + # Add another config entry to the sensor device + other_config_entry = MockConfigEntry() + other_config_entry.add_to_hass(hass) + device_registry.async_update_device( + sensor_device.id, add_config_entry_id=other_config_entry.entry_id + ) + + assert await hass.config_entries.async_setup(utility_meter_config_entry.entry_id) + await hass.async_block_till_done() + + events = {} + for ( + utility_meter_entity + ) in entity_registry.entities.get_entries_for_config_entry_id( + utility_meter_config_entry.entry_id + ): + assert utility_meter_entity.device_id == sensor_entity_entry.device_id + events[utility_meter_entity.entity_id] = track_entity_registry_actions( + hass, utility_meter_entity.entity_id + ) + assert set(events) == expected_entities + + sensor_device = device_registry.async_get(sensor_device.id) + assert utility_meter_config_entry.entry_id in sensor_device.config_entries + + # Remove the source sensor's config entry from the device, this removes the + # source sensor + with patch( + "homeassistant.components.utility_meter.async_unload_entry", + wraps=utility_meter.async_unload_entry, + ) as mock_unload_entry: + device_registry.async_update_device( + sensor_device.id, remove_config_entry_id=sensor_config_entry.entry_id + ) + await hass.async_block_till_done() + await hass.async_block_till_done() + mock_unload_entry.assert_not_called() + + # Check that the utility_meter config entry is removed from the device + sensor_device = device_registry.async_get(sensor_device.id) + assert utility_meter_config_entry.entry_id not in sensor_device.config_entries + + # Check that the utility_meter config entry is not removed + assert utility_meter_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + for entity_events in events.values(): + assert entity_events == ["update"] + + +@pytest.mark.parametrize( + ("tariffs", "expected_entities"), + [ + ([], {"sensor.my_utility_meter"}), + ( + ["peak", "offpeak"], + { + "select.my_utility_meter", + "sensor.my_utility_meter_offpeak", + "sensor.my_utility_meter_peak", + }, + ), + ], +) +async def test_async_handle_source_entity_changes_source_entity_removed_from_device( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + utility_meter_config_entry: MockConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, + expected_entities: set[str], +) -> None: + """Test the source entity removed from the source device.""" + assert await hass.config_entries.async_setup(utility_meter_config_entry.entry_id) + await hass.async_block_till_done() + + events = {} + for ( + utility_meter_entity + ) in entity_registry.entities.get_entries_for_config_entry_id( + utility_meter_config_entry.entry_id + ): + assert utility_meter_entity.device_id == sensor_entity_entry.device_id + events[utility_meter_entity.entity_id] = track_entity_registry_actions( + hass, utility_meter_entity.entity_id + ) + assert set(events) == expected_entities + + sensor_device = device_registry.async_get(sensor_device.id) + assert utility_meter_config_entry.entry_id in sensor_device.config_entries + + # Remove the source sensor from the device + with patch( + "homeassistant.components.utility_meter.async_unload_entry", + wraps=utility_meter.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + sensor_entity_entry.entity_id, device_id=None + ) + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the utility_meter config entry is removed from the device + sensor_device = device_registry.async_get(sensor_device.id) + assert utility_meter_config_entry.entry_id not in sensor_device.config_entries + + # Check that the utility_meter config entry is not removed + assert utility_meter_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + for entity_events in events.values(): + assert entity_events == ["update"] + + +@pytest.mark.parametrize( + ("tariffs", "expected_entities"), + [ + ([], {"sensor.my_utility_meter"}), + ( + ["peak", "offpeak"], + { + "select.my_utility_meter", + "sensor.my_utility_meter_offpeak", + "sensor.my_utility_meter_peak", + }, + ), + ], +) +async def test_async_handle_source_entity_changes_source_entity_moved_other_device( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + utility_meter_config_entry: MockConfigEntry, + sensor_config_entry: ConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, + expected_entities: set[str], +) -> None: + """Test the source entity is moved to another device.""" + sensor_device_2 = device_registry.async_get_or_create( + config_entry_id=sensor_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:FF")}, + ) + + assert await hass.config_entries.async_setup(utility_meter_config_entry.entry_id) + await hass.async_block_till_done() + + events = {} + for ( + utility_meter_entity + ) in entity_registry.entities.get_entries_for_config_entry_id( + utility_meter_config_entry.entry_id + ): + assert utility_meter_entity.device_id == sensor_entity_entry.device_id + events[utility_meter_entity.entity_id] = track_entity_registry_actions( + hass, utility_meter_entity.entity_id + ) + assert set(events) == expected_entities + + sensor_device = device_registry.async_get(sensor_device.id) + assert utility_meter_config_entry.entry_id in sensor_device.config_entries + sensor_device_2 = device_registry.async_get(sensor_device_2.id) + assert utility_meter_config_entry.entry_id not in sensor_device_2.config_entries + + # Move the source sensor to another device + with patch( + "homeassistant.components.utility_meter.async_unload_entry", + wraps=utility_meter.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + sensor_entity_entry.entity_id, device_id=sensor_device_2.id + ) + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the utility_meter config entry is moved to the other device + sensor_device = device_registry.async_get(sensor_device.id) + assert utility_meter_config_entry.entry_id not in sensor_device.config_entries + sensor_device_2 = device_registry.async_get(sensor_device_2.id) + assert utility_meter_config_entry.entry_id in sensor_device_2.config_entries + + # Check that the utility_meter config entry is not removed + assert utility_meter_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + for entity_events in events.values(): + assert entity_events == ["update"] + + +@pytest.mark.parametrize( + ("tariffs", "expected_entities"), + [ + ([], {"sensor.my_utility_meter"}), + ( + ["peak", "offpeak"], + { + "select.my_utility_meter", + "sensor.my_utility_meter_offpeak", + "sensor.my_utility_meter_peak", + }, + ), + ], +) +async def test_async_handle_source_entity_new_entity_id( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + utility_meter_config_entry: MockConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, + expected_entities: set[str], +) -> None: + """Test the source entity's entity ID is changed.""" + assert await hass.config_entries.async_setup(utility_meter_config_entry.entry_id) + await hass.async_block_till_done() + + events = {} + for ( + utility_meter_entity + ) in entity_registry.entities.get_entries_for_config_entry_id( + utility_meter_config_entry.entry_id + ): + assert utility_meter_entity.device_id == sensor_entity_entry.device_id + events[utility_meter_entity.entity_id] = track_entity_registry_actions( + hass, utility_meter_entity.entity_id + ) + assert set(events) == expected_entities + + sensor_device = device_registry.async_get(sensor_device.id) + assert utility_meter_config_entry.entry_id in sensor_device.config_entries + + # Change the source entity's entity ID + with patch( + "homeassistant.components.utility_meter.async_unload_entry", + wraps=utility_meter.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + sensor_entity_entry.entity_id, new_entity_id="sensor.new_entity_id" + ) + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the utility_meter config entry is updated with the new entity ID + assert utility_meter_config_entry.options["source"] == "sensor.new_entity_id" + + # Check that the helper config is still in the device + sensor_device = device_registry.async_get(sensor_device.id) + assert utility_meter_config_entry.entry_id in sensor_device.config_entries + + # Check that the utility_meter config entry is not removed + assert utility_meter_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + for entity_events in events.values(): + assert entity_events == [] From bcedb06862e6ac57e9c1891e385a2060fb9b2134 Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Wed, 11 Jun 2025 15:10:00 +0200 Subject: [PATCH 185/266] Bump linkplay to v0.2.11 (#146530) --- homeassistant/components/linkplay/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/linkplay/manifest.json b/homeassistant/components/linkplay/manifest.json index 1bbf70ed3ac..d6319c7a506 100644 --- a/homeassistant/components/linkplay/manifest.json +++ b/homeassistant/components/linkplay/manifest.json @@ -7,6 +7,6 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["linkplay"], - "requirements": ["python-linkplay==0.2.10"], + "requirements": ["python-linkplay==0.2.11"], "zeroconf": ["_linkplay._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index d9a9014a6b9..500fa0676ab 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2452,7 +2452,7 @@ python-juicenet==1.1.0 python-kasa[speedups]==0.10.2 # homeassistant.components.linkplay -python-linkplay==0.2.10 +python-linkplay==0.2.11 # homeassistant.components.lirc # python-lirc==1.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 66f34dd6d69..1136433d059 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2022,7 +2022,7 @@ python-juicenet==1.1.0 python-kasa[speedups]==0.10.2 # homeassistant.components.linkplay -python-linkplay==0.2.10 +python-linkplay==0.2.11 # homeassistant.components.lirc # python-lirc==1.2.3 From 91e296a0c81ad41879949943817ad978d3db7e31 Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Wed, 11 Jun 2025 16:46:52 +0300 Subject: [PATCH 186/266] Bump hdate to 1.1.1 (#146536) --- homeassistant/components/jewish_calendar/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/jewish_calendar/manifest.json b/homeassistant/components/jewish_calendar/manifest.json index c93844dd559..550a6514593 100644 --- a/homeassistant/components/jewish_calendar/manifest.json +++ b/homeassistant/components/jewish_calendar/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/jewish_calendar", "iot_class": "calculated", "loggers": ["hdate"], - "requirements": ["hdate[astral]==1.1.0"], + "requirements": ["hdate[astral]==1.1.1"], "single_config_entry": true } diff --git a/requirements_all.txt b/requirements_all.txt index 500fa0676ab..c98f22f9d91 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1133,7 +1133,7 @@ hass-splunk==0.1.1 hassil==2.2.3 # homeassistant.components.jewish_calendar -hdate[astral]==1.1.0 +hdate[astral]==1.1.1 # homeassistant.components.heatmiser heatmiserV3==2.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1136433d059..d1265ec260d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -988,7 +988,7 @@ hass-nabucasa==0.101.0 hassil==2.2.3 # homeassistant.components.jewish_calendar -hdate[astral]==1.1.0 +hdate[astral]==1.1.1 # homeassistant.components.here_travel_time here-routing==1.0.1 From 232f853d6804d717671cf3c929a7450e12a117a2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 11 Jun 2025 12:27:51 +0200 Subject: [PATCH 187/266] Simplify helper_integration.async_handle_source_entity_changes (#146516) --- homeassistant/components/derivative/__init__.py | 8 -------- homeassistant/components/switch_as_x/__init__.py | 8 +------- homeassistant/helpers/helper_integration.py | 14 ++++++-------- tests/helpers/test_helper_integration.py | 7 ------- 4 files changed, 7 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/derivative/__init__.py b/homeassistant/components/derivative/__init__.py index 6d539817875..0806a8f824d 100644 --- a/homeassistant/components/derivative/__init__.py +++ b/homeassistant/components/derivative/__init__.py @@ -2,19 +2,15 @@ from __future__ import annotations -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_SOURCE, Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er from homeassistant.helpers.device import ( async_entity_id_to_device_id, async_remove_stale_devices_links_keep_entity_device, ) from homeassistant.helpers.helper_integration import async_handle_source_entity_changes -from .const import DOMAIN - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Derivative from a config entry.""" @@ -33,14 +29,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # The source entity has been removed, we need to clean the device links. async_remove_stale_devices_links_keep_entity_device(hass, entry.entry_id, None) - entity_registry = er.async_get(hass) entry.async_on_unload( async_handle_source_entity_changes( hass, helper_config_entry_id=entry.entry_id, - get_helper_entity_id=lambda: entity_registry.async_get_entity_id( - SENSOR_DOMAIN, DOMAIN, entry.entry_id - ), set_source_entity_id_or_uuid=set_source_entity_id_or_uuid, source_device_id=async_entity_id_to_device_id( hass, entry.options[CONF_SOURCE] diff --git a/homeassistant/components/switch_as_x/__init__.py b/homeassistant/components/switch_as_x/__init__.py index 7d12ae4aec2..c77eda9b294 100644 --- a/homeassistant/components/switch_as_x/__init__.py +++ b/homeassistant/components/switch_as_x/__init__.py @@ -13,10 +13,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.helper_integration import async_handle_source_entity_changes -from .const import CONF_INVERT, CONF_TARGET_DOMAIN, DOMAIN -from .light import LightSwitch - -__all__ = ["LightSwitch"] +from .const import CONF_INVERT, CONF_TARGET_DOMAIN _LOGGER = logging.getLogger(__name__) @@ -72,9 +69,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async_handle_source_entity_changes( hass, helper_config_entry_id=entry.entry_id, - get_helper_entity_id=lambda: entity_registry.async_get_entity_id( - entry.options[CONF_TARGET_DOMAIN], DOMAIN, entry.entry_id - ), set_source_entity_id_or_uuid=set_source_entity_id_or_uuid, source_device_id=async_add_to_device(hass, entry, entity_id), source_entity_id_or_uuid=entry.options[CONF_ENTITY_ID], diff --git a/homeassistant/helpers/helper_integration.py b/homeassistant/helpers/helper_integration.py index 37aa246178e..61bb0bcd45d 100644 --- a/homeassistant/helpers/helper_integration.py +++ b/homeassistant/helpers/helper_integration.py @@ -15,7 +15,6 @@ def async_handle_source_entity_changes( hass: HomeAssistant, *, helper_config_entry_id: str, - get_helper_entity_id: Callable[[], str | None], set_source_entity_id_or_uuid: Callable[[str], None], source_device_id: str | None, source_entity_id_or_uuid: str, @@ -37,8 +36,6 @@ def async_handle_source_entity_changes( to no device, and the helper config entry removed from the old device. Then the helper config entry is reloaded. - :param get_helper_entity: A function which returns the helper entity's entity ID, - or None if the helper entity does not exist. :param set_source_entity_id_or_uuid: A function which updates the source entity ID or UUID, e.g., in the helper config entry options. :param source_entity_removed: A function which is called when the source entity @@ -81,13 +78,14 @@ def async_handle_source_entity_changes( return # The source entity has been moved to a different device, update the helper - # helper entity to link to the new device and the helper device to include - # the helper config entry - helper_entity_id = get_helper_entity_id() - if helper_entity_id: + # entities to link to the new device and the helper device to include the + # helper config entry + for helper_entity in entity_registry.entities.get_entries_for_config_entry_id( + helper_config_entry_id + ): # Update the helper entity to link to the new device (or no device) entity_registry.async_update_entity( - helper_entity_id, device_id=source_entity_entry.device_id + helper_entity.entity_id, device_id=source_entity_entry.device_id ) if source_entity_entry.device_id is not None: diff --git a/tests/helpers/test_helper_integration.py b/tests/helpers/test_helper_integration.py index 12433894dc7..47f1b62feb7 100644 --- a/tests/helpers/test_helper_integration.py +++ b/tests/helpers/test_helper_integration.py @@ -156,18 +156,11 @@ def mock_helper_integration( ) -> None: """Mock the helper integration.""" - def get_helper_entity_id() -> str | None: - """Get the helper entity ID.""" - return entity_registry.async_get_entity_id( - "sensor", HELPER_DOMAIN, helper_config_entry.entry_id - ) - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Mock setup entry.""" async_handle_source_entity_changes( hass, helper_config_entry_id=helper_config_entry.entry_id, - get_helper_entity_id=get_helper_entity_id, set_source_entity_id_or_uuid=set_source_entity_id_or_uuid, source_device_id=source_entity_entry.device_id, source_entity_id_or_uuid=helper_config_entry.options["source"], From c02707a90f09e30ba1eab5a7c8c990f19de4569b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 11 Jun 2025 15:17:19 +0200 Subject: [PATCH 188/266] Handle changes to source entity in statistics helper (#146523) --- .../components/statistics/__init__.py | 26 ++ tests/components/statistics/test_init.py | 283 +++++++++++++++++- 2 files changed, 305 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/statistics/__init__.py b/homeassistant/components/statistics/__init__.py index f71274e0ee7..f800c82f1f9 100644 --- a/homeassistant/components/statistics/__init__.py +++ b/homeassistant/components/statistics/__init__.py @@ -4,8 +4,10 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ENTITY_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.device import ( + async_entity_id_to_device_id, async_remove_stale_devices_links_keep_entity_device, ) +from homeassistant.helpers.helper_integration import async_handle_source_entity_changes DOMAIN = "statistics" PLATFORMS = [Platform.SENSOR] @@ -20,6 +22,30 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.options[CONF_ENTITY_ID], ) + def set_source_entity_id_or_uuid(source_entity_id: str) -> None: + hass.config_entries.async_update_entry( + entry, + options={**entry.options, CONF_ENTITY_ID: source_entity_id}, + ) + + async def source_entity_removed() -> None: + # The source entity has been removed, we remove the config entry because + # statistics does not allow replacing the input entity. + await hass.config_entries.async_remove(entry.entry_id) + + entry.async_on_unload( + async_handle_source_entity_changes( + hass, + helper_config_entry_id=entry.entry_id, + set_source_entity_id_or_uuid=set_source_entity_id_or_uuid, + source_device_id=async_entity_id_to_device_id( + hass, entry.options[CONF_ENTITY_ID] + ), + source_entity_id_or_uuid=entry.options[CONF_ENTITY_ID], + source_entity_removed=source_entity_removed, + ) + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(update_listener)) diff --git a/tests/components/statistics/test_init.py b/tests/components/statistics/test_init.py index 64829ea7d66..c11045a2eb2 100644 --- a/tests/components/statistics/test_init.py +++ b/tests/components/statistics/test_init.py @@ -2,14 +2,98 @@ from __future__ import annotations -from homeassistant.components.statistics import DOMAIN as STATISTICS_DOMAIN -from homeassistant.config_entries import ConfigEntryState -from homeassistant.core import HomeAssistant +from unittest.mock import patch + +import pytest + +from homeassistant.components import statistics +from homeassistant.components.statistics import DOMAIN +from homeassistant.components.statistics.config_flow import StatisticsConfigFlowHandler +from homeassistant.config_entries import ConfigEntry, ConfigEntryState +from homeassistant.core import Event, HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.event import async_track_entity_registry_updated_event from tests.common import MockConfigEntry +@pytest.fixture +def sensor_config_entry(hass: HomeAssistant) -> er.RegistryEntry: + """Fixture to create a sensor config entry.""" + sensor_config_entry = MockConfigEntry() + sensor_config_entry.add_to_hass(hass) + return sensor_config_entry + + +@pytest.fixture +def sensor_device( + device_registry: dr.DeviceRegistry, sensor_config_entry: ConfigEntry +) -> dr.DeviceEntry: + """Fixture to create a sensor device.""" + return device_registry.async_get_or_create( + config_entry_id=sensor_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + + +@pytest.fixture +def sensor_entity_entry( + entity_registry: er.EntityRegistry, + sensor_config_entry: ConfigEntry, + sensor_device: dr.DeviceEntry, +) -> er.RegistryEntry: + """Fixture to create a sensor entity entry.""" + return entity_registry.async_get_or_create( + "sensor", + "test", + "unique", + config_entry=sensor_config_entry, + device_id=sensor_device.id, + original_name="ABC", + ) + + +@pytest.fixture +def statistics_config_entry( + hass: HomeAssistant, + sensor_entity_entry: er.RegistryEntry, +) -> MockConfigEntry: + """Fixture to create a statistics config entry.""" + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "name": "My statistics", + "entity_id": sensor_entity_entry.entity_id, + "state_characteristic": "mean", + "keep_last_sample": False, + "percentile": 50.0, + "precision": 2.0, + "sampling_size": 20.0, + }, + title="My statistics", + version=StatisticsConfigFlowHandler.VERSION, + minor_version=StatisticsConfigFlowHandler.MINOR_VERSION, + ) + + config_entry.add_to_hass(hass) + + return config_entry + + +def track_entity_registry_actions(hass: HomeAssistant, entity_id: str) -> list[str]: + """Track entity registry actions for an entity.""" + events = [] + + def add_event(event: Event[er.EventEntityRegistryUpdatedData]) -> None: + """Add entity registry updated event to the list.""" + events.append(event.data["action"]) + + async_track_entity_registry_updated_event(hass, entity_id, add_event) + + return events + + async def test_unload_entry(hass: HomeAssistant, loaded_entry: MockConfigEntry) -> None: """Test unload an entry.""" @@ -51,7 +135,7 @@ async def test_device_cleaning( # Configure the configuration entry for Statistics statistics_config_entry = MockConfigEntry( data={}, - domain=STATISTICS_DOMAIN, + domain=DOMAIN, options={ "name": "Statistics", "entity_id": "sensor.test_source", @@ -107,3 +191,194 @@ async def test_device_cleaning( assert len(devices_after_reload) == 1 assert devices_after_reload[0].id == source_device1_entry.id + + +async def test_async_handle_source_entity_changes_source_entity_removed( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + statistics_config_entry: MockConfigEntry, + sensor_config_entry: ConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, +) -> None: + """Test the statistics config entry is removed when the source entity is removed.""" + # Add another config entry to the sensor device + other_config_entry = MockConfigEntry() + other_config_entry.add_to_hass(hass) + device_registry.async_update_device( + sensor_device.id, add_config_entry_id=other_config_entry.entry_id + ) + + assert await hass.config_entries.async_setup(statistics_config_entry.entry_id) + await hass.async_block_till_done() + + statistics_entity_entry = entity_registry.async_get("sensor.my_statistics") + assert statistics_entity_entry.device_id == sensor_entity_entry.device_id + + sensor_device = device_registry.async_get(sensor_device.id) + assert statistics_config_entry.entry_id in sensor_device.config_entries + + events = track_entity_registry_actions(hass, statistics_entity_entry.entity_id) + + # Remove the source sensor's config entry from the device, this removes the + # source sensor + with patch( + "homeassistant.components.statistics.async_unload_entry", + wraps=statistics.async_unload_entry, + ) as mock_unload_entry: + device_registry.async_update_device( + sensor_device.id, remove_config_entry_id=sensor_config_entry.entry_id + ) + await hass.async_block_till_done() + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the statistics config entry is removed from the device + sensor_device = device_registry.async_get(sensor_device.id) + assert statistics_config_entry.entry_id not in sensor_device.config_entries + + # Check that the statistics config entry is removed + assert statistics_config_entry.entry_id not in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == ["remove"] + + +async def test_async_handle_source_entity_changes_source_entity_removed_from_device( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + statistics_config_entry: MockConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, +) -> None: + """Test the source entity removed from the source device.""" + assert await hass.config_entries.async_setup(statistics_config_entry.entry_id) + await hass.async_block_till_done() + + statistics_entity_entry = entity_registry.async_get("sensor.my_statistics") + assert statistics_entity_entry.device_id == sensor_entity_entry.device_id + + sensor_device = device_registry.async_get(sensor_device.id) + assert statistics_config_entry.entry_id in sensor_device.config_entries + + events = track_entity_registry_actions(hass, statistics_entity_entry.entity_id) + + # Remove the source sensor from the device + with patch( + "homeassistant.components.statistics.async_unload_entry", + wraps=statistics.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + sensor_entity_entry.entity_id, device_id=None + ) + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the statistics config entry is removed from the device + sensor_device = device_registry.async_get(sensor_device.id) + assert statistics_config_entry.entry_id not in sensor_device.config_entries + + # Check that the statistics config entry is not removed + assert statistics_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == ["update"] + + +async def test_async_handle_source_entity_changes_source_entity_moved_other_device( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + statistics_config_entry: MockConfigEntry, + sensor_config_entry: ConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, +) -> None: + """Test the source entity is moved to another device.""" + sensor_device_2 = device_registry.async_get_or_create( + config_entry_id=sensor_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:FF")}, + ) + + assert await hass.config_entries.async_setup(statistics_config_entry.entry_id) + await hass.async_block_till_done() + + statistics_entity_entry = entity_registry.async_get("sensor.my_statistics") + assert statistics_entity_entry.device_id == sensor_entity_entry.device_id + + sensor_device = device_registry.async_get(sensor_device.id) + assert statistics_config_entry.entry_id in sensor_device.config_entries + sensor_device_2 = device_registry.async_get(sensor_device_2.id) + assert statistics_config_entry.entry_id not in sensor_device_2.config_entries + + events = track_entity_registry_actions(hass, statistics_entity_entry.entity_id) + + # Move the source sensor to another device + with patch( + "homeassistant.components.statistics.async_unload_entry", + wraps=statistics.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + sensor_entity_entry.entity_id, device_id=sensor_device_2.id + ) + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the statistics config entry is moved to the other device + sensor_device = device_registry.async_get(sensor_device.id) + assert statistics_config_entry.entry_id not in sensor_device.config_entries + sensor_device_2 = device_registry.async_get(sensor_device_2.id) + assert statistics_config_entry.entry_id in sensor_device_2.config_entries + + # Check that the statistics config entry is not removed + assert statistics_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == ["update"] + + +async def test_async_handle_source_entity_new_entity_id( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + statistics_config_entry: MockConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, +) -> None: + """Test the source entity's entity ID is changed.""" + assert await hass.config_entries.async_setup(statistics_config_entry.entry_id) + await hass.async_block_till_done() + + statistics_entity_entry = entity_registry.async_get("sensor.my_statistics") + assert statistics_entity_entry.device_id == sensor_entity_entry.device_id + + sensor_device = device_registry.async_get(sensor_device.id) + assert statistics_config_entry.entry_id in sensor_device.config_entries + + events = track_entity_registry_actions(hass, statistics_entity_entry.entity_id) + + # Change the source entity's entity ID + with patch( + "homeassistant.components.statistics.async_unload_entry", + wraps=statistics.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + sensor_entity_entry.entity_id, new_entity_id="sensor.new_entity_id" + ) + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the statistics config entry is updated with the new entity ID + assert statistics_config_entry.options["entity_id"] == "sensor.new_entity_id" + + # Check that the helper config is still in the device + sensor_device = device_registry.async_get(sensor_device.id) + assert statistics_config_entry.entry_id in sensor_device.config_entries + + # Check that the statistics config entry is not removed + assert statistics_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == [] From e73bcc73b58360377dd04b839987c0d0cd3d3016 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 11 Jun 2025 17:26:20 +0000 Subject: [PATCH 189/266] Bump version to 2025.6.0b8 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 6cbe42d6dbe..8c4fd0bf774 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 = 6 -PATCH_VERSION: Final = "0b7" +PATCH_VERSION: Final = "0b8" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index f0aa8169054..21717881d35 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.6.0b7" +version = "2025.6.0b8" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From fd605e0abe97d7176aa1f1e376ec2f0dc5e2e1a3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 11 Jun 2025 15:18:04 +0200 Subject: [PATCH 190/266] Handle changes to source entities in generic_hygrostat helper (#146538) --- .../components/generic_hygrostat/__init__.py | 59 ++- .../components/generic_hygrostat/test_init.py | 428 +++++++++++++++++- 2 files changed, 480 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/generic_hygrostat/__init__.py b/homeassistant/components/generic_hygrostat/__init__.py index b4a6014c5a4..a12994c1a75 100644 --- a/homeassistant/components/generic_hygrostat/__init__.py +++ b/homeassistant/components/generic_hygrostat/__init__.py @@ -5,11 +5,18 @@ import voluptuous as vol from homeassistant.components.humidifier import HumidifierDeviceClass from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, CONF_UNIQUE_ID, Platform -from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv, discovery +from homeassistant.core import Event, HomeAssistant +from homeassistant.helpers import ( + config_validation as cv, + discovery, + entity_registry as er, +) from homeassistant.helpers.device import ( + async_entity_id_to_device_id, async_remove_stale_devices_links_keep_entity_device, ) +from homeassistant.helpers.event import async_track_entity_registry_updated_event +from homeassistant.helpers.helper_integration import async_handle_source_entity_changes from homeassistant.helpers.typing import ConfigType DOMAIN = "generic_hygrostat" @@ -88,6 +95,54 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.options[CONF_HUMIDIFIER], ) + def set_humidifier_entity_id_or_uuid(source_entity_id: str) -> None: + hass.config_entries.async_update_entry( + entry, + options={**entry.options, CONF_HUMIDIFIER: source_entity_id}, + ) + + async def source_entity_removed() -> None: + # The source entity has been removed, we need to clean the device links. + async_remove_stale_devices_links_keep_entity_device(hass, entry.entry_id, None) + + entry.async_on_unload( + # We use async_handle_source_entity_changes to track changes to the humidifer, + # but not the humidity sensor because the generic_hygrostat adds itself to the + # humidifier's device. + async_handle_source_entity_changes( + hass, + helper_config_entry_id=entry.entry_id, + set_source_entity_id_or_uuid=set_humidifier_entity_id_or_uuid, + source_device_id=async_entity_id_to_device_id( + hass, entry.options[CONF_HUMIDIFIER] + ), + source_entity_id_or_uuid=entry.options[CONF_HUMIDIFIER], + source_entity_removed=source_entity_removed, + ) + ) + + async def async_sensor_updated( + event: Event[er.EventEntityRegistryUpdatedData], + ) -> None: + """Handle entity registry update.""" + data = event.data + if data["action"] != "update": + return + if "entity_id" not in data["changes"]: + return + + # Entity_id changed, update the config entry + hass.config_entries.async_update_entry( + entry, + options={**entry.options, CONF_SENSOR: data["entity_id"]}, + ) + + entry.async_on_unload( + async_track_entity_registry_updated_event( + hass, entry.options[CONF_SENSOR], async_sensor_updated + ) + ) + await hass.config_entries.async_forward_entry_setups(entry, (Platform.HUMIDIFIER,)) entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) return True diff --git a/tests/components/generic_hygrostat/test_init.py b/tests/components/generic_hygrostat/test_init.py index bd4792f939d..254d4da5806 100644 --- a/tests/components/generic_hygrostat/test_init.py +++ b/tests/components/generic_hygrostat/test_init.py @@ -2,17 +2,136 @@ from __future__ import annotations -from homeassistant.components.generic_hygrostat import ( - DOMAIN as GENERIC_HYDROSTAT_DOMAIN, -) -from homeassistant.core import HomeAssistant +from unittest.mock import patch + +import pytest + +from homeassistant.components import generic_hygrostat +from homeassistant.components.generic_hygrostat import DOMAIN +from homeassistant.components.generic_hygrostat.config_flow import ConfigFlowHandler +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import Event, HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.event import async_track_entity_registry_updated_event from .test_humidifier import ENT_SENSOR from tests.common import MockConfigEntry +@pytest.fixture +def sensor_config_entry(hass: HomeAssistant) -> er.RegistryEntry: + """Fixture to create a sensor config entry.""" + sensor_config_entry = MockConfigEntry() + sensor_config_entry.add_to_hass(hass) + return sensor_config_entry + + +@pytest.fixture +def sensor_device( + device_registry: dr.DeviceRegistry, sensor_config_entry: ConfigEntry +) -> dr.DeviceEntry: + """Fixture to create a sensor device.""" + return device_registry.async_get_or_create( + config_entry_id=sensor_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EE")}, + ) + + +@pytest.fixture +def sensor_entity_entry( + entity_registry: er.EntityRegistry, + sensor_config_entry: ConfigEntry, + sensor_device: dr.DeviceEntry, +) -> er.RegistryEntry: + """Fixture to create a sensor entity entry.""" + return entity_registry.async_get_or_create( + "sensor", + "test", + "unique", + config_entry=sensor_config_entry, + device_id=sensor_device.id, + original_name="ABC", + ) + + +@pytest.fixture +def switch_config_entry(hass: HomeAssistant) -> er.RegistryEntry: + """Fixture to create a switch config entry.""" + switch_config_entry = MockConfigEntry() + switch_config_entry.add_to_hass(hass) + return switch_config_entry + + +@pytest.fixture +def switch_device( + device_registry: dr.DeviceRegistry, switch_config_entry: ConfigEntry +) -> dr.DeviceEntry: + """Fixture to create a switch device.""" + return device_registry.async_get_or_create( + config_entry_id=switch_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + + +@pytest.fixture +def switch_entity_entry( + entity_registry: er.EntityRegistry, + switch_config_entry: ConfigEntry, + switch_device: dr.DeviceEntry, +) -> er.RegistryEntry: + """Fixture to create a switch entity entry.""" + return entity_registry.async_get_or_create( + "switch", + "test", + "unique", + config_entry=switch_config_entry, + device_id=switch_device.id, + original_name="ABC", + ) + + +@pytest.fixture +def generic_hygrostat_config_entry( + hass: HomeAssistant, + sensor_entity_entry: er.RegistryEntry, + switch_entity_entry: er.RegistryEntry, +) -> MockConfigEntry: + """Fixture to create a generic_hygrostat config entry.""" + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "device_class": "humidifier", + "dry_tolerance": 2.0, + "humidifier": switch_entity_entry.entity_id, + "name": "My generic hygrostat", + "target_sensor": sensor_entity_entry.entity_id, + "wet_tolerance": 4.0, + }, + title="My generic hygrostat", + version=ConfigFlowHandler.VERSION, + minor_version=ConfigFlowHandler.MINOR_VERSION, + ) + + config_entry.add_to_hass(hass) + + return config_entry + + +def track_entity_registry_actions(hass: HomeAssistant, entity_id: str) -> list[str]: + """Track entity registry actions for an entity.""" + events = [] + + def add_event(event: Event[er.EventEntityRegistryUpdatedData]) -> None: + """Add entity registry updated event to the list.""" + events.append(event.data["action"]) + + async_track_entity_registry_updated_event(hass, entity_id, add_event) + + return events + + async def test_device_cleaning( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -45,7 +164,7 @@ async def test_device_cleaning( # Configure the configuration entry for helper helper_config_entry = MockConfigEntry( data={}, - domain=GENERIC_HYDROSTAT_DOMAIN, + domain=DOMAIN, options={ "device_class": "humidifier", "dry_tolerance": 2.0, @@ -100,3 +219,302 @@ async def test_device_cleaning( assert len(devices_after_reload) == 1 assert devices_after_reload[0].id == source_device1_entry.id + + +@pytest.mark.usefixtures( + "sensor_config_entry", + "sensor_device", + "sensor_entity_entry", + "switch_config_entry", + "switch_device", +) +@pytest.mark.parametrize( + ("source_entity_id", "helper_in_device", "expected_events"), + [("switch.test_unique", True, ["update"]), ("sensor.test_unique", False, [])], +) +async def test_async_handle_source_entity_changes_source_entity_removed( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + generic_hygrostat_config_entry: MockConfigEntry, + switch_entity_entry: er.RegistryEntry, + source_entity_id: str, + helper_in_device: bool, + expected_events: list[str], +) -> None: + """Test the generic_hygrostat config entry is removed when the source entity is removed.""" + source_entity_entry = entity_registry.async_get(source_entity_id) + + # Add another config entry to the source device + other_config_entry = MockConfigEntry() + other_config_entry.add_to_hass(hass) + device_registry.async_update_device( + source_entity_entry.device_id, add_config_entry_id=other_config_entry.entry_id + ) + + assert await hass.config_entries.async_setup( + generic_hygrostat_config_entry.entry_id + ) + await hass.async_block_till_done() + + generic_hygrostat_entity_entry = entity_registry.async_get( + "humidifier.my_generic_hygrostat" + ) + assert generic_hygrostat_entity_entry.device_id == switch_entity_entry.device_id + + source_device = device_registry.async_get(source_entity_entry.device_id) + assert ( + generic_hygrostat_config_entry.entry_id in source_device.config_entries + ) == helper_in_device + + events = track_entity_registry_actions( + hass, generic_hygrostat_entity_entry.entity_id + ) + + # Remove the source entity's config entry from the device, this removes the + # source entity + with patch( + "homeassistant.components.generic_hygrostat.async_unload_entry", + wraps=generic_hygrostat.async_unload_entry, + ) as mock_unload_entry: + device_registry.async_update_device( + source_device.id, remove_config_entry_id=source_entity_entry.config_entry_id + ) + await hass.async_block_till_done() + await hass.async_block_till_done() + mock_unload_entry.assert_not_called() + + # Check if the generic_hygrostat config entry is not in the device + source_device = device_registry.async_get(source_device.id) + assert generic_hygrostat_config_entry.entry_id not in source_device.config_entries + + # Check that the generic_hygrostat config entry is not removed + assert ( + generic_hygrostat_config_entry.entry_id in hass.config_entries.async_entry_ids() + ) + + # Check we got the expected events + assert events == expected_events + + +@pytest.mark.usefixtures( + "sensor_config_entry", + "sensor_device", + "sensor_entity_entry", + "switch_config_entry", + "switch_device", +) +@pytest.mark.parametrize( + ("source_entity_id", "helper_in_device", "unload_entry_calls", "expected_events"), + [("switch.test_unique", True, 1, ["update"]), ("sensor.test_unique", False, 0, [])], +) +async def test_async_handle_source_entity_changes_source_entity_removed_from_device( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + generic_hygrostat_config_entry: MockConfigEntry, + switch_entity_entry: er.RegistryEntry, + source_entity_id: str, + helper_in_device: bool, + unload_entry_calls: int, + expected_events: list[str], +) -> None: + """Test the source entity removed from the source device.""" + source_entity_entry = entity_registry.async_get(source_entity_id) + + assert await hass.config_entries.async_setup( + generic_hygrostat_config_entry.entry_id + ) + await hass.async_block_till_done() + + generic_hygrostat_entity_entry = entity_registry.async_get( + "humidifier.my_generic_hygrostat" + ) + assert generic_hygrostat_entity_entry.device_id == switch_entity_entry.device_id + + source_device = device_registry.async_get(source_entity_entry.device_id) + assert ( + generic_hygrostat_config_entry.entry_id in source_device.config_entries + ) == helper_in_device + + events = track_entity_registry_actions( + hass, generic_hygrostat_entity_entry.entity_id + ) + + # Remove the source entity from the device + with patch( + "homeassistant.components.generic_hygrostat.async_unload_entry", + wraps=generic_hygrostat.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + source_entity_entry.entity_id, device_id=None + ) + await hass.async_block_till_done() + assert len(mock_unload_entry.mock_calls) == unload_entry_calls + + # Check that the generic_hygrostat config entry is removed from the device + source_device = device_registry.async_get(source_device.id) + assert generic_hygrostat_config_entry.entry_id not in source_device.config_entries + + # Check that the generic_hygrostat config entry is not removed + assert ( + generic_hygrostat_config_entry.entry_id in hass.config_entries.async_entry_ids() + ) + + # Check we got the expected events + assert events == expected_events + + +@pytest.mark.usefixtures( + "sensor_config_entry", + "sensor_device", + "sensor_entity_entry", + "switch_config_entry", + "switch_device", +) +@pytest.mark.parametrize( + ("source_entity_id", "helper_in_device", "unload_entry_calls", "expected_events"), + [("switch.test_unique", True, 1, ["update"]), ("sensor.test_unique", False, 0, [])], +) +async def test_async_handle_source_entity_changes_source_entity_moved_other_device( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + generic_hygrostat_config_entry: MockConfigEntry, + switch_entity_entry: er.RegistryEntry, + source_entity_id: str, + helper_in_device: bool, + unload_entry_calls: int, + expected_events: list[str], +) -> None: + """Test the source entity is moved to another device.""" + source_entity_entry = entity_registry.async_get(source_entity_id) + + source_device_2 = device_registry.async_get_or_create( + config_entry_id=source_entity_entry.config_entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:FF")}, + ) + + assert await hass.config_entries.async_setup( + generic_hygrostat_config_entry.entry_id + ) + await hass.async_block_till_done() + + generic_hygrostat_entity_entry = entity_registry.async_get( + "humidifier.my_generic_hygrostat" + ) + assert generic_hygrostat_entity_entry.device_id == switch_entity_entry.device_id + + source_device = device_registry.async_get(source_entity_entry.device_id) + assert ( + generic_hygrostat_config_entry.entry_id in source_device.config_entries + ) == helper_in_device + source_device_2 = device_registry.async_get(source_device_2.id) + assert generic_hygrostat_config_entry.entry_id not in source_device_2.config_entries + + events = track_entity_registry_actions( + hass, generic_hygrostat_entity_entry.entity_id + ) + + # Move the source entity to another device + with patch( + "homeassistant.components.generic_hygrostat.async_unload_entry", + wraps=generic_hygrostat.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + source_entity_entry.entity_id, device_id=source_device_2.id + ) + await hass.async_block_till_done() + assert len(mock_unload_entry.mock_calls) == unload_entry_calls + + # Check that the generic_hygrostat config entry is moved to the other device + source_device = device_registry.async_get(source_device.id) + assert generic_hygrostat_config_entry.entry_id not in source_device.config_entries + source_device_2 = device_registry.async_get(source_device_2.id) + assert ( + generic_hygrostat_config_entry.entry_id in source_device_2.config_entries + ) == helper_in_device + + # Check that the generic_hygrostat config entry is not removed + assert ( + generic_hygrostat_config_entry.entry_id in hass.config_entries.async_entry_ids() + ) + + # Check we got the expected events + assert events == expected_events + + +@pytest.mark.usefixtures( + "sensor_config_entry", + "sensor_device", + "sensor_entity_entry", + "switch_config_entry", + "switch_device", +) +@pytest.mark.parametrize( + ("source_entity_id", "new_entity_id", "helper_in_device", "config_key"), + [ + ("switch.test_unique", "switch.new_entity_id", True, "humidifier"), + ("sensor.test_unique", "sensor.new_entity_id", False, "target_sensor"), + ], +) +async def test_async_handle_source_entity_new_entity_id( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + generic_hygrostat_config_entry: MockConfigEntry, + switch_entity_entry: er.RegistryEntry, + source_entity_id: str, + new_entity_id: str, + helper_in_device: bool, + config_key: str, +) -> None: + """Test the source entity's entity ID is changed.""" + source_entity_entry = entity_registry.async_get(source_entity_id) + + assert await hass.config_entries.async_setup( + generic_hygrostat_config_entry.entry_id + ) + await hass.async_block_till_done() + + generic_hygrostat_entity_entry = entity_registry.async_get( + "humidifier.my_generic_hygrostat" + ) + assert generic_hygrostat_entity_entry.device_id == switch_entity_entry.device_id + + source_device = device_registry.async_get(source_entity_entry.device_id) + assert ( + generic_hygrostat_config_entry.entry_id in source_device.config_entries + ) == helper_in_device + + events = track_entity_registry_actions( + hass, generic_hygrostat_entity_entry.entity_id + ) + + # Change the source entity's entity ID + with patch( + "homeassistant.components.generic_hygrostat.async_unload_entry", + wraps=generic_hygrostat.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + source_entity_entry.entity_id, new_entity_id=new_entity_id + ) + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the generic_hygrostat config entry is updated with the new entity ID + assert generic_hygrostat_config_entry.options[config_key] == new_entity_id + + # Check that the helper config is still in the device + source_device = device_registry.async_get(source_device.id) + assert ( + generic_hygrostat_config_entry.entry_id in source_device.config_entries + ) == helper_in_device + + # Check that the generic_hygrostat config entry is not removed + assert ( + generic_hygrostat_config_entry.entry_id in hass.config_entries.async_entry_ids() + ) + + # Check we got the expected events + assert events == [] From 89637a618eb82626c2b70ac902418e7e85efac71 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 11 Jun 2025 15:26:52 +0200 Subject: [PATCH 191/266] Handle changes to source entities in generic_thermostat helper (#146541) --- .../components/generic_thermostat/__init__.py | 57 ++- .../generic_thermostat/test_init.py | 428 +++++++++++++++++- 2 files changed, 482 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/generic_thermostat/__init__.py b/homeassistant/components/generic_thermostat/__init__.py index dc43049a262..3e2af8598de 100644 --- a/homeassistant/components/generic_thermostat/__init__.py +++ b/homeassistant/components/generic_thermostat/__init__.py @@ -1,12 +1,16 @@ """The generic_thermostat component.""" from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import Event, HomeAssistant +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.device import ( + async_entity_id_to_device_id, async_remove_stale_devices_links_keep_entity_device, ) +from homeassistant.helpers.event import async_track_entity_registry_updated_event +from homeassistant.helpers.helper_integration import async_handle_source_entity_changes -from .const import CONF_HEATER, PLATFORMS +from .const import CONF_HEATER, CONF_SENSOR, PLATFORMS async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -17,6 +21,55 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.entry_id, entry.options[CONF_HEATER], ) + + def set_humidifier_entity_id_or_uuid(source_entity_id: str) -> None: + hass.config_entries.async_update_entry( + entry, + options={**entry.options, CONF_HEATER: source_entity_id}, + ) + + async def source_entity_removed() -> None: + # The source entity has been removed, we need to clean the device links. + async_remove_stale_devices_links_keep_entity_device(hass, entry.entry_id, None) + + entry.async_on_unload( + # We use async_handle_source_entity_changes to track changes to the heater, but + # not the temperature sensor because the generic_hygrostat adds itself to the + # heater's device. + async_handle_source_entity_changes( + hass, + helper_config_entry_id=entry.entry_id, + set_source_entity_id_or_uuid=set_humidifier_entity_id_or_uuid, + source_device_id=async_entity_id_to_device_id( + hass, entry.options[CONF_HEATER] + ), + source_entity_id_or_uuid=entry.options[CONF_HEATER], + source_entity_removed=source_entity_removed, + ) + ) + + async def async_sensor_updated( + event: Event[er.EventEntityRegistryUpdatedData], + ) -> None: + """Handle entity registry update.""" + data = event.data + if data["action"] != "update": + return + if "entity_id" not in data["changes"]: + return + + # Entity_id changed, update the config entry + hass.config_entries.async_update_entry( + entry, + options={**entry.options, CONF_SENSOR: data["entity_id"]}, + ) + + entry.async_on_unload( + async_track_entity_registry_updated_event( + hass, entry.options[CONF_SENSOR], async_sensor_updated + ) + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) return True diff --git a/tests/components/generic_thermostat/test_init.py b/tests/components/generic_thermostat/test_init.py index addae2f684e..9131e3ffdd4 100644 --- a/tests/components/generic_thermostat/test_init.py +++ b/tests/components/generic_thermostat/test_init.py @@ -2,13 +2,134 @@ from __future__ import annotations +from unittest.mock import patch + +import pytest + +from homeassistant.components import generic_thermostat +from homeassistant.components.generic_thermostat.config_flow import ConfigFlowHandler from homeassistant.components.generic_thermostat.const import DOMAIN -from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import Event, HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.event import async_track_entity_registry_updated_event from tests.common import MockConfigEntry +@pytest.fixture +def sensor_config_entry(hass: HomeAssistant) -> er.RegistryEntry: + """Fixture to create a sensor config entry.""" + sensor_config_entry = MockConfigEntry() + sensor_config_entry.add_to_hass(hass) + return sensor_config_entry + + +@pytest.fixture +def sensor_device( + device_registry: dr.DeviceRegistry, sensor_config_entry: ConfigEntry +) -> dr.DeviceEntry: + """Fixture to create a sensor device.""" + return device_registry.async_get_or_create( + config_entry_id=sensor_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EE")}, + ) + + +@pytest.fixture +def sensor_entity_entry( + entity_registry: er.EntityRegistry, + sensor_config_entry: ConfigEntry, + sensor_device: dr.DeviceEntry, +) -> er.RegistryEntry: + """Fixture to create a sensor entity entry.""" + return entity_registry.async_get_or_create( + "sensor", + "test", + "unique", + config_entry=sensor_config_entry, + device_id=sensor_device.id, + original_name="ABC", + ) + + +@pytest.fixture +def switch_config_entry(hass: HomeAssistant) -> er.RegistryEntry: + """Fixture to create a switch config entry.""" + switch_config_entry = MockConfigEntry() + switch_config_entry.add_to_hass(hass) + return switch_config_entry + + +@pytest.fixture +def switch_device( + device_registry: dr.DeviceRegistry, switch_config_entry: ConfigEntry +) -> dr.DeviceEntry: + """Fixture to create a switch device.""" + return device_registry.async_get_or_create( + config_entry_id=switch_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + + +@pytest.fixture +def switch_entity_entry( + entity_registry: er.EntityRegistry, + switch_config_entry: ConfigEntry, + switch_device: dr.DeviceEntry, +) -> er.RegistryEntry: + """Fixture to create a switch entity entry.""" + return entity_registry.async_get_or_create( + "switch", + "test", + "unique", + config_entry=switch_config_entry, + device_id=switch_device.id, + original_name="ABC", + ) + + +@pytest.fixture +def generic_thermostat_config_entry( + hass: HomeAssistant, + sensor_entity_entry: er.RegistryEntry, + switch_entity_entry: er.RegistryEntry, +) -> MockConfigEntry: + """Fixture to create a generic_thermostat config entry.""" + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "name": "My generic thermostat", + "heater": switch_entity_entry.entity_id, + "target_sensor": sensor_entity_entry.entity_id, + "ac_mode": False, + "cold_tolerance": 0.3, + "hot_tolerance": 0.3, + }, + title="My generic thermostat", + version=ConfigFlowHandler.VERSION, + minor_version=ConfigFlowHandler.MINOR_VERSION, + ) + + config_entry.add_to_hass(hass) + + return config_entry + + +def track_entity_registry_actions(hass: HomeAssistant, entity_id: str) -> list[str]: + """Track entity registry actions for an entity.""" + events = [] + + def add_event(event: Event[er.EventEntityRegistryUpdatedData]) -> None: + """Add entity registry updated event to the list.""" + events.append(event.data["action"]) + + async_track_entity_registry_updated_event(hass, entity_id, add_event) + + return events + + async def test_device_cleaning( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -96,3 +217,308 @@ async def test_device_cleaning( assert len(devices_after_reload) == 1 assert devices_after_reload[0].id == source_device1_entry.id + + +@pytest.mark.usefixtures( + "sensor_config_entry", + "sensor_device", + "sensor_entity_entry", + "switch_config_entry", + "switch_device", +) +@pytest.mark.parametrize( + ("source_entity_id", "helper_in_device", "expected_events"), + [("switch.test_unique", True, ["update"]), ("sensor.test_unique", False, [])], +) +async def test_async_handle_source_entity_changes_source_entity_removed( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + generic_thermostat_config_entry: MockConfigEntry, + switch_entity_entry: er.RegistryEntry, + source_entity_id: str, + helper_in_device: bool, + expected_events: list[str], +) -> None: + """Test the generic_thermostat config entry is removed when the source entity is removed.""" + source_entity_entry = entity_registry.async_get(source_entity_id) + + # Add another config entry to the source device + other_config_entry = MockConfigEntry() + other_config_entry.add_to_hass(hass) + device_registry.async_update_device( + source_entity_entry.device_id, add_config_entry_id=other_config_entry.entry_id + ) + + assert await hass.config_entries.async_setup( + generic_thermostat_config_entry.entry_id + ) + await hass.async_block_till_done() + + generic_thermostat_entity_entry = entity_registry.async_get( + "climate.my_generic_thermostat" + ) + assert generic_thermostat_entity_entry.device_id == switch_entity_entry.device_id + + source_device = device_registry.async_get(source_entity_entry.device_id) + assert ( + generic_thermostat_config_entry.entry_id in source_device.config_entries + ) == helper_in_device + + events = track_entity_registry_actions( + hass, generic_thermostat_entity_entry.entity_id + ) + + # Remove the source entity's config entry from the device, this removes the + # source entity + with patch( + "homeassistant.components.generic_thermostat.async_unload_entry", + wraps=generic_thermostat.async_unload_entry, + ) as mock_unload_entry: + device_registry.async_update_device( + source_device.id, remove_config_entry_id=source_entity_entry.config_entry_id + ) + await hass.async_block_till_done() + await hass.async_block_till_done() + mock_unload_entry.assert_not_called() + + # Check if the generic_thermostat config entry is not in the device + source_device = device_registry.async_get(source_device.id) + assert generic_thermostat_config_entry.entry_id not in source_device.config_entries + + # Check that the generic_thermostat config entry is not removed + assert ( + generic_thermostat_config_entry.entry_id + in hass.config_entries.async_entry_ids() + ) + + # Check we got the expected events + assert events == expected_events + + +@pytest.mark.usefixtures( + "sensor_config_entry", + "sensor_device", + "sensor_entity_entry", + "switch_config_entry", + "switch_device", +) +@pytest.mark.parametrize( + ("source_entity_id", "helper_in_device", "unload_entry_calls", "expected_events"), + [("switch.test_unique", True, 1, ["update"]), ("sensor.test_unique", False, 0, [])], +) +async def test_async_handle_source_entity_changes_source_entity_removed_from_device( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + generic_thermostat_config_entry: MockConfigEntry, + switch_entity_entry: er.RegistryEntry, + source_entity_id: str, + helper_in_device: bool, + unload_entry_calls: int, + expected_events: list[str], +) -> None: + """Test the source entity removed from the source device.""" + source_entity_entry = entity_registry.async_get(source_entity_id) + + assert await hass.config_entries.async_setup( + generic_thermostat_config_entry.entry_id + ) + await hass.async_block_till_done() + + generic_thermostat_entity_entry = entity_registry.async_get( + "climate.my_generic_thermostat" + ) + assert generic_thermostat_entity_entry.device_id == switch_entity_entry.device_id + + source_device = device_registry.async_get(source_entity_entry.device_id) + assert ( + generic_thermostat_config_entry.entry_id in source_device.config_entries + ) == helper_in_device + + events = track_entity_registry_actions( + hass, generic_thermostat_entity_entry.entity_id + ) + + # Remove the source entity from the device + with patch( + "homeassistant.components.generic_thermostat.async_unload_entry", + wraps=generic_thermostat.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + source_entity_entry.entity_id, device_id=None + ) + await hass.async_block_till_done() + assert len(mock_unload_entry.mock_calls) == unload_entry_calls + + # Check that the generic_thermostat config entry is removed from the device + source_device = device_registry.async_get(source_device.id) + assert generic_thermostat_config_entry.entry_id not in source_device.config_entries + + # Check that the generic_thermostat config entry is not removed + assert ( + generic_thermostat_config_entry.entry_id + in hass.config_entries.async_entry_ids() + ) + + # Check we got the expected events + assert events == expected_events + + +@pytest.mark.usefixtures( + "sensor_config_entry", + "sensor_device", + "sensor_entity_entry", + "switch_config_entry", + "switch_device", +) +@pytest.mark.parametrize( + ("source_entity_id", "helper_in_device", "unload_entry_calls", "expected_events"), + [("switch.test_unique", True, 1, ["update"]), ("sensor.test_unique", False, 0, [])], +) +async def test_async_handle_source_entity_changes_source_entity_moved_other_device( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + generic_thermostat_config_entry: MockConfigEntry, + switch_entity_entry: er.RegistryEntry, + source_entity_id: str, + helper_in_device: bool, + unload_entry_calls: int, + expected_events: list[str], +) -> None: + """Test the source entity is moved to another device.""" + source_entity_entry = entity_registry.async_get(source_entity_id) + + source_device_2 = device_registry.async_get_or_create( + config_entry_id=source_entity_entry.config_entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:FF")}, + ) + + assert await hass.config_entries.async_setup( + generic_thermostat_config_entry.entry_id + ) + await hass.async_block_till_done() + + generic_thermostat_entity_entry = entity_registry.async_get( + "climate.my_generic_thermostat" + ) + assert generic_thermostat_entity_entry.device_id == switch_entity_entry.device_id + + source_device = device_registry.async_get(source_entity_entry.device_id) + assert ( + generic_thermostat_config_entry.entry_id in source_device.config_entries + ) == helper_in_device + source_device_2 = device_registry.async_get(source_device_2.id) + assert ( + generic_thermostat_config_entry.entry_id not in source_device_2.config_entries + ) + + events = track_entity_registry_actions( + hass, generic_thermostat_entity_entry.entity_id + ) + + # Move the source entity to another device + with patch( + "homeassistant.components.generic_thermostat.async_unload_entry", + wraps=generic_thermostat.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + source_entity_entry.entity_id, device_id=source_device_2.id + ) + await hass.async_block_till_done() + assert len(mock_unload_entry.mock_calls) == unload_entry_calls + + # Check that the generic_thermostat config entry is moved to the other device + source_device = device_registry.async_get(source_device.id) + assert generic_thermostat_config_entry.entry_id not in source_device.config_entries + source_device_2 = device_registry.async_get(source_device_2.id) + assert ( + generic_thermostat_config_entry.entry_id in source_device_2.config_entries + ) == helper_in_device + + # Check that the generic_thermostat config entry is not removed + assert ( + generic_thermostat_config_entry.entry_id + in hass.config_entries.async_entry_ids() + ) + + # Check we got the expected events + assert events == expected_events + + +@pytest.mark.usefixtures( + "sensor_config_entry", + "sensor_device", + "sensor_entity_entry", + "switch_config_entry", + "switch_device", +) +@pytest.mark.parametrize( + ("source_entity_id", "new_entity_id", "helper_in_device", "config_key"), + [ + ("switch.test_unique", "switch.new_entity_id", True, "heater"), + ("sensor.test_unique", "sensor.new_entity_id", False, "target_sensor"), + ], +) +async def test_async_handle_source_entity_new_entity_id( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + generic_thermostat_config_entry: MockConfigEntry, + switch_entity_entry: er.RegistryEntry, + source_entity_id: str, + new_entity_id: str, + helper_in_device: bool, + config_key: str, +) -> None: + """Test the source entity's entity ID is changed.""" + source_entity_entry = entity_registry.async_get(source_entity_id) + + assert await hass.config_entries.async_setup( + generic_thermostat_config_entry.entry_id + ) + await hass.async_block_till_done() + + generic_thermostat_entity_entry = entity_registry.async_get( + "climate.my_generic_thermostat" + ) + assert generic_thermostat_entity_entry.device_id == switch_entity_entry.device_id + + source_device = device_registry.async_get(source_entity_entry.device_id) + assert ( + generic_thermostat_config_entry.entry_id in source_device.config_entries + ) == helper_in_device + + events = track_entity_registry_actions( + hass, generic_thermostat_entity_entry.entity_id + ) + + # Change the source entity's entity ID + with patch( + "homeassistant.components.generic_thermostat.async_unload_entry", + wraps=generic_thermostat.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + source_entity_entry.entity_id, new_entity_id=new_entity_id + ) + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the generic_thermostat config entry is updated with the new entity ID + assert generic_thermostat_config_entry.options[config_key] == new_entity_id + + # Check that the helper config is still in the device + source_device = device_registry.async_get(source_device.id) + assert ( + generic_thermostat_config_entry.entry_id in source_device.config_entries + ) == helper_in_device + + # Check that the generic_thermostat config entry is not removed + assert ( + generic_thermostat_config_entry.entry_id + in hass.config_entries.async_entry_ids() + ) + + # Check we got the expected events + assert events == [] From 43797c03cccf42f0df991607e099edea05cd1552 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 11 Jun 2025 15:46:19 +0200 Subject: [PATCH 192/266] Update frontend to 20250531.1 (#146542) --- 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 7282482f329..5c3b8ed2264 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==20250531.0"] + "requirements": ["home-assistant-frontend==20250531.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b5d1af412f9..921de18a732 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ habluetooth==3.49.0 hass-nabucasa==0.101.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250531.0 +home-assistant-frontend==20250531.1 home-assistant-intents==2025.6.10 httpx==0.28.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index c98f22f9d91..91268677b43 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1164,7 +1164,7 @@ hole==0.8.0 holidays==0.74 # homeassistant.components.frontend -home-assistant-frontend==20250531.0 +home-assistant-frontend==20250531.1 # homeassistant.components.conversation home-assistant-intents==2025.6.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d1265ec260d..a9ac282ad4e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1010,7 +1010,7 @@ hole==0.8.0 holidays==0.74 # homeassistant.components.frontend -home-assistant-frontend==20250531.0 +home-assistant-frontend==20250531.1 # homeassistant.components.conversation home-assistant-intents==2025.6.10 From 1f221712a25635d68f975d3b38e0378ab36d1f7f Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Wed, 11 Jun 2025 16:39:02 +0300 Subject: [PATCH 193/266] Remove the Delete button on the ZwaveJS device page (#146544) --- homeassistant/components/zwave_js/__init__.py | 32 ------------------- tests/components/zwave_js/test_init.py | 21 ------------ 2 files changed, 53 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index abbf10fb494..e8f2bf6f2d4 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -1119,38 +1119,6 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: LOGGER.error(err) -async def async_remove_config_entry_device( - hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry -) -> bool: - """Remove a config entry from a device.""" - client: ZwaveClient = config_entry.runtime_data[DATA_CLIENT] - - # Driver may not be ready yet so we can't allow users to remove a device since - # we need to check if the device is still known to the controller - if (driver := client.driver) is None: - LOGGER.error("Driver for %s is not ready", config_entry.title) - return False - - # If a node is found on the controller that matches the hardware based identifier - # on the device, prevent the device from being removed. - if next( - ( - node - for node in driver.controller.nodes.values() - if get_device_id_ext(driver, node) in device_entry.identifiers - ), - None, - ): - return False - - controller_events: ControllerEvents = config_entry.runtime_data[ - DATA_DRIVER_EVENTS - ].controller_events - controller_events.registered_unique_ids.pop(device_entry.id, None) - controller_events.discovered_value_ids.pop(device_entry.id, None) - return True - - async def async_ensure_addon_running(hass: HomeAssistant, entry: ConfigEntry) -> None: """Ensure that Z-Wave JS add-on is installed and running.""" addon_manager = _get_addon_manager(hass) diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index a0423efdf52..ef74373ad9e 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -1692,27 +1692,6 @@ async def test_replace_different_node( (DOMAIN, multisensor_6_device_id_ext), } - ws_client = await hass_ws_client(hass) - - # Simulate the driver not being ready to ensure that the device removal handler - # does not crash - driver = client.driver - client.driver = None - - response = await ws_client.remove_device(hank_device.id, integration.entry_id) - assert not response["success"] - - client.driver = driver - - # Attempting to remove the hank device should pass, but removing the multisensor should not - response = await ws_client.remove_device(hank_device.id, integration.entry_id) - assert response["success"] - - response = await ws_client.remove_device( - multisensor_6_device.id, integration.entry_id - ) - assert not response["success"] - async def test_node_model_change( hass: HomeAssistant, From 75e6f23a82bf40f41913a4bf6d9991a4444068e5 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 11 Jun 2025 17:12:34 +0200 Subject: [PATCH 194/266] Update frontend to 20250531.2 (#146551) --- 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 5c3b8ed2264..4299d2b7503 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==20250531.1"] + "requirements": ["home-assistant-frontend==20250531.2"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 921de18a732..d0904cd12b3 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ habluetooth==3.49.0 hass-nabucasa==0.101.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250531.1 +home-assistant-frontend==20250531.2 home-assistant-intents==2025.6.10 httpx==0.28.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 91268677b43..c73829cf379 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1164,7 +1164,7 @@ hole==0.8.0 holidays==0.74 # homeassistant.components.frontend -home-assistant-frontend==20250531.1 +home-assistant-frontend==20250531.2 # homeassistant.components.conversation home-assistant-intents==2025.6.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a9ac282ad4e..6af2c4ecc6e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1010,7 +1010,7 @@ hole==0.8.0 holidays==0.74 # homeassistant.components.frontend -home-assistant-frontend==20250531.1 +home-assistant-frontend==20250531.2 # homeassistant.components.conversation home-assistant-intents==2025.6.10 From 60b8230eccb213b16faf92a9c7e117247f020906 Mon Sep 17 00:00:00 2001 From: andreimoraru Date: Wed, 11 Jun 2025 18:53:25 +0300 Subject: [PATCH 195/266] Bump yt-dlp to 2025.06.09 (#146553) * Bumped yt-dlp to 2025.06.09 * fix --------- 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 3ce80f497ef..20068efccef 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.05.22"], + "requirements": ["yt-dlp[default]==2025.06.09"], "single_config_entry": true } diff --git a/requirements_all.txt b/requirements_all.txt index c73829cf379..b70c806c1be 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3162,7 +3162,7 @@ youless-api==2.2.0 youtubeaio==1.1.5 # homeassistant.components.media_extractor -yt-dlp[default]==2025.05.22 +yt-dlp[default]==2025.06.09 # homeassistant.components.zabbix zabbix-utils==2.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6af2c4ecc6e..39fe3779466 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2606,7 +2606,7 @@ youless-api==2.2.0 youtubeaio==1.1.5 # homeassistant.components.media_extractor -yt-dlp[default]==2025.05.22 +yt-dlp[default]==2025.06.09 # homeassistant.components.zamg zamg==0.3.6 From 02524b8b9b287556c65c0d361422ab536cefdd83 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 11 Jun 2025 18:39:46 +0200 Subject: [PATCH 196/266] Make issue creation check architecture instead of uname (#146537) --- homeassistant/components/hassio/__init__.py | 35 ++++-- .../components/homeassistant/__init__.py | 30 +++-- .../components/homeassistant/strings.json | 4 +- tests/components/hassio/conftest.py | 13 ++ tests/components/hassio/test_init.py | 113 ++++++++++++++---- tests/components/homeassistant/test_init.py | 110 +++++++++-------- 6 files changed, 202 insertions(+), 103 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 041877e3944..6772034e53f 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -9,8 +9,10 @@ from functools import partial import logging import os import re +import struct from typing import Any, NamedTuple +import aiofiles from aiohasupervisor import SupervisorError import voluptuous as vol @@ -56,7 +58,6 @@ from homeassistant.helpers.issue_registry import IssueSeverity from homeassistant.helpers.service_info.hassio import ( HassioServiceInfo as _HassioServiceInfo, ) -from homeassistant.helpers.system_info import async_get_system_info from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from homeassistant.util.async_ import create_eager_task @@ -233,6 +234,17 @@ SCHEMA_RESTORE_PARTIAL = SCHEMA_RESTORE_FULL.extend( ) +def _is_32_bit() -> bool: + size = struct.calcsize("P") + return size * 8 == 32 + + +async def _get_arch() -> str: + async with aiofiles.open("/etc/apk/arch") as arch_file: + raw_arch = await arch_file.read() + return {"x86": "i386"}.get(raw_arch, raw_arch) + + class APIEndpointSettings(NamedTuple): """Settings for API endpoint.""" @@ -554,7 +566,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data[ADDONS_COORDINATOR] = coordinator - system_info = await async_get_system_info(hass) + arch = await _get_arch() def deprecated_setup_issue() -> None: os_info = get_os_info(hass) @@ -562,20 +574,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if os_info is None or info is None: return is_haos = info.get("hassos") is not None - arch = system_info["arch"] board = os_info.get("board") - supported_board = board in {"rpi3", "rpi4", "tinker", "odroid-xu4", "rpi2"} - if is_haos and arch == "armv7" and supported_board: + unsupported_board = board in {"tinker", "odroid-xu4", "rpi2"} + unsupported_os_on_board = board in {"rpi3", "rpi4"} + if is_haos and (unsupported_board or unsupported_os_on_board): issue_id = "deprecated_os_" - if board in {"rpi3", "rpi4"}: + if unsupported_os_on_board: issue_id += "aarch64" - elif board in {"tinker", "odroid-xu4", "rpi2"}: + elif unsupported_board: issue_id += "armv7" ir.async_create_issue( hass, "homeassistant", issue_id, - breaks_in_ha_version="2025.12.0", learn_more_url=DEPRECATION_URL, is_fixable=False, severity=IssueSeverity.WARNING, @@ -584,9 +595,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "installation_guide": "https://www.home-assistant.io/installation/", }, ) - deprecated_architecture = False - if arch in {"i386", "armhf"} or (arch == "armv7" and not supported_board): - deprecated_architecture = True + bit32 = _is_32_bit() + deprecated_architecture = bit32 and not ( + unsupported_board or unsupported_os_on_board + ) if not is_haos or deprecated_architecture: issue_id = "deprecated" if not is_haos: @@ -597,7 +609,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, "homeassistant", issue_id, - breaks_in_ha_version="2025.12.0", learn_more_url=DEPRECATION_URL, is_fixable=False, severity=IssueSeverity.WARNING, diff --git a/homeassistant/components/homeassistant/__init__.py b/homeassistant/components/homeassistant/__init__.py index 1433358b568..4360fa9c16e 100644 --- a/homeassistant/components/homeassistant/__init__.py +++ b/homeassistant/components/homeassistant/__init__.py @@ -4,8 +4,10 @@ import asyncio from collections.abc import Callable, Coroutine import itertools as it import logging +import struct from typing import Any +import aiofiles import voluptuous as vol from homeassistant import config as conf_util, core_config @@ -94,6 +96,17 @@ DEPRECATION_URL = ( ) +def _is_32_bit() -> bool: + size = struct.calcsize("P") + return size * 8 == 32 + + +async def _get_arch() -> str: + async with aiofiles.open("/etc/apk/arch") as arch_file: + raw_arch = (await arch_file.read()).strip() + return {"x86": "i386", "x86_64": "amd64"}.get(raw_arch, raw_arch) + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: C901 """Set up general services related to Home Assistant.""" @@ -403,23 +416,21 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: installation_type = info["installation_type"][15:] if installation_type in {"Core", "Container"}: deprecated_method = installation_type == "Core" + bit32 = _is_32_bit() arch = info["arch"] - if arch == "armv7" and installation_type == "Container": + if bit32 and installation_type == "Container": + arch = await _get_arch() ir.async_create_issue( hass, DOMAIN, - "deprecated_container_armv7", - breaks_in_ha_version="2025.12.0", + "deprecated_container", learn_more_url=DEPRECATION_URL, is_fixable=False, severity=IssueSeverity.WARNING, - translation_key="deprecated_container_armv7", + translation_key="deprecated_container", + translation_placeholders={"arch": arch}, ) - deprecated_architecture = False - if arch in {"i386", "armhf"} or ( - arch == "armv7" and installation_type != "Container" - ): - deprecated_architecture = True + deprecated_architecture = bit32 and installation_type != "Container" if deprecated_method or deprecated_architecture: issue_id = "deprecated" if deprecated_method: @@ -430,7 +441,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: hass, DOMAIN, issue_id, - breaks_in_ha_version="2025.12.0", learn_more_url=DEPRECATION_URL, is_fixable=False, severity=IssueSeverity.WARNING, diff --git a/homeassistant/components/homeassistant/strings.json b/homeassistant/components/homeassistant/strings.json index 93b4105c702..940af999c4d 100644 --- a/homeassistant/components/homeassistant/strings.json +++ b/homeassistant/components/homeassistant/strings.json @@ -107,9 +107,9 @@ "title": "Deprecation notice: 32-bit architecture", "description": "This system uses 32-bit hardware (`{arch}`), which has been deprecated and will no longer receive updates after the release of Home Assistant 2025.12. As your hardware is no longer capable of running newer versions of Home Assistant, you will need to migrate to new hardware." }, - "deprecated_container_armv7": { + "deprecated_container": { "title": "[%key:component::homeassistant::issues::deprecated_architecture::title%]", - "description": "This system is running on a 32-bit operating system (`armv7`), which has been deprecated and will no longer receive updates after the release of Home Assistant 2025.12. Check if your system is capable of running a 64-bit operating system. If not, you will need to migrate to new hardware." + "description": "This system is running on a 32-bit operating system (`{arch}`), which has been deprecated and will no longer receive updates after the release of Home Assistant 2025.12. Check if your system is capable of running a 64-bit operating system. If not, you will need to migrate to new hardware." }, "deprecated_os_aarch64": { "title": "[%key:component::homeassistant::issues::deprecated_architecture::title%]", diff --git a/tests/components/hassio/conftest.py b/tests/components/hassio/conftest.py index a71ee370b32..56f7ffaa5b9 100644 --- a/tests/components/hassio/conftest.py +++ b/tests/components/hassio/conftest.py @@ -260,3 +260,16 @@ def all_setup_requests( }, }, ) + + +@pytest.fixture +def arch() -> str: + """Arch found in apk file.""" + return "amd64" + + +@pytest.fixture(autouse=True) +def mock_arch_file(arch: str) -> Generator[None]: + """Mock arch file.""" + with patch("homeassistant.components.hassio._get_arch", return_value=arch): + yield diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index f74ed852a49..f424beedc85 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -1156,7 +1156,11 @@ def test_deprecated_constants( ("rpi2", "deprecated_os_armv7"), ], ) -async def test_deprecated_installation_issue_aarch64( +@pytest.mark.parametrize( + "arch", + ["armv7"], +) +async def test_deprecated_installation_issue_os_armv7( hass: HomeAssistant, issue_registry: ir.IssueRegistry, freezer: FrozenDateTimeFactory, @@ -1167,18 +1171,15 @@ async def test_deprecated_installation_issue_aarch64( with ( patch.dict(os.environ, MOCK_ENVIRON), patch( - "homeassistant.components.hassio.async_get_system_info", + "homeassistant.components.homeassistant.async_get_system_info", return_value={ "installation_type": "Home Assistant OS", "arch": "armv7", }, ), patch( - "homeassistant.components.homeassistant.async_get_system_info", - return_value={ - "installation_type": "Home Assistant OS", - "arch": "armv7", - }, + "homeassistant.components.hassio._is_32_bit", + return_value=True, ), patch( "homeassistant.components.hassio.get_os_info", return_value={"board": board} @@ -1228,7 +1229,7 @@ async def test_deprecated_installation_issue_aarch64( "armv7", ], ) -async def test_deprecated_installation_issue_32bit_method( +async def test_deprecated_installation_issue_32bit_os( hass: HomeAssistant, issue_registry: ir.IssueRegistry, freezer: FrozenDateTimeFactory, @@ -1238,18 +1239,15 @@ async def test_deprecated_installation_issue_32bit_method( with ( patch.dict(os.environ, MOCK_ENVIRON), patch( - "homeassistant.components.hassio.async_get_system_info", + "homeassistant.components.homeassistant.async_get_system_info", return_value={ "installation_type": "Home Assistant OS", "arch": arch, }, ), patch( - "homeassistant.components.homeassistant.async_get_system_info", - return_value={ - "installation_type": "Home Assistant OS", - "arch": arch, - }, + "homeassistant.components.hassio._is_32_bit", + return_value=True, ), patch( "homeassistant.components.hassio.get_os_info", @@ -1308,18 +1306,15 @@ async def test_deprecated_installation_issue_32bit_supervised( with ( patch.dict(os.environ, MOCK_ENVIRON), patch( - "homeassistant.components.hassio.async_get_system_info", + "homeassistant.components.homeassistant.async_get_system_info", return_value={ "installation_type": "Home Assistant Supervised", "arch": arch, }, ), patch( - "homeassistant.components.homeassistant.async_get_system_info", - return_value={ - "installation_type": "Home Assistant Supervised", - "arch": arch, - }, + "homeassistant.components.hassio._is_32_bit", + return_value=True, ), patch( "homeassistant.components.hassio.get_os_info", @@ -1365,6 +1360,75 @@ async def test_deprecated_installation_issue_32bit_supervised( } +@pytest.mark.parametrize( + "arch", + [ + "amd64", + "aarch64", + ], +) +async def test_deprecated_installation_issue_64bit_supervised( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, + freezer: FrozenDateTimeFactory, + arch: str, +) -> None: + """Test deprecated architecture issue.""" + with ( + patch.dict(os.environ, MOCK_ENVIRON), + patch( + "homeassistant.components.homeassistant.async_get_system_info", + return_value={ + "installation_type": "Home Assistant Supervised", + "arch": arch, + }, + ), + patch( + "homeassistant.components.hassio._is_32_bit", + return_value=False, + ), + patch( + "homeassistant.components.hassio.get_os_info", + return_value={"board": "generic-x86-64"}, + ), + patch( + "homeassistant.components.hassio.get_info", return_value={"hassos": None} + ), + patch("homeassistant.components.hardware.async_setup", return_value=True), + ): + assert await async_setup_component(hass, "homeassistant", {}) + config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + freezer.tick(REQUEST_REFRESH_DELAY) + async_fire_time_changed(hass) + await hass.async_block_till_done() + await hass.services.async_call( + "homeassistant", + "update_entity", + { + "entity_id": [ + "update.home_assistant_core_update", + "update.home_assistant_supervisor_update", + ] + }, + blocking=True, + ) + freezer.tick(HASSIO_UPDATE_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert len(issue_registry.issues) == 1 + issue = issue_registry.async_get_issue("homeassistant", "deprecated_method") + assert issue.domain == "homeassistant" + assert issue.severity == ir.IssueSeverity.WARNING + assert issue.translation_placeholders == { + "installation_type": "Supervised", + "arch": arch, + } + + @pytest.mark.parametrize( ("board", "issue_id"), [ @@ -1382,18 +1446,15 @@ async def test_deprecated_installation_issue_supported_board( with ( patch.dict(os.environ, MOCK_ENVIRON), patch( - "homeassistant.components.hassio.async_get_system_info", + "homeassistant.components.homeassistant.async_get_system_info", return_value={ "installation_type": "Home Assistant OS", "arch": "aarch64", }, ), patch( - "homeassistant.components.homeassistant.async_get_system_info", - return_value={ - "installation_type": "Home Assistant OS", - "arch": "aarch64", - }, + "homeassistant.components.hassio._is_32_bit", + return_value=False, ), patch( "homeassistant.components.hassio.get_os_info", return_value={"board": board} diff --git a/tests/components/homeassistant/test_init.py b/tests/components/homeassistant/test_init.py index 0010422cd28..0646b4dcfa6 100644 --- a/tests/components/homeassistant/test_init.py +++ b/tests/components/homeassistant/test_init.py @@ -648,18 +648,24 @@ async def test_reload_all( "armv7", ], ) -async def test_deprecated_installation_issue_32bit_method( +async def test_deprecated_installation_issue_32bit_core( hass: HomeAssistant, issue_registry: ir.IssueRegistry, arch: str, ) -> None: """Test deprecated installation issue.""" - with patch( - "homeassistant.components.homeassistant.async_get_system_info", - return_value={ - "installation_type": "Home Assistant Core", - "arch": arch, - }, + with ( + patch( + "homeassistant.components.homeassistant.async_get_system_info", + return_value={ + "installation_type": "Home Assistant Core", + "arch": arch, + }, + ), + patch( + "homeassistant.components.homeassistant._is_32_bit", + return_value=True, + ), ): assert await async_setup_component(hass, HOMEASSISTANT_DOMAIN, {}) await hass.async_block_till_done() @@ -679,48 +685,28 @@ async def test_deprecated_installation_issue_32bit_method( @pytest.mark.parametrize( "arch", [ - "i386", - "armhf", + "aarch64", + "generic-x86-64", ], ) -async def test_deprecated_installation_issue_32bit( +async def test_deprecated_installation_issue_64bit_core( hass: HomeAssistant, issue_registry: ir.IssueRegistry, arch: str, ) -> None: """Test deprecated installation issue.""" - with patch( - "homeassistant.components.homeassistant.async_get_system_info", - return_value={ - "installation_type": "Home Assistant Container", - "arch": arch, - }, - ): - assert await async_setup_component(hass, HOMEASSISTANT_DOMAIN, {}) - await hass.async_block_till_done() - - assert len(issue_registry.issues) == 1 - issue = issue_registry.async_get_issue( - HOMEASSISTANT_DOMAIN, "deprecated_architecture" - ) - assert issue.domain == HOMEASSISTANT_DOMAIN - assert issue.severity == ir.IssueSeverity.WARNING - assert issue.translation_placeholders == { - "installation_type": "Container", - "arch": arch, - } - - -async def test_deprecated_installation_issue_method( - hass: HomeAssistant, issue_registry: ir.IssueRegistry -) -> None: - """Test deprecated installation issue.""" - with patch( - "homeassistant.components.homeassistant.async_get_system_info", - return_value={ - "installation_type": "Home Assistant Core", - "arch": "generic-x86-64", - }, + with ( + patch( + "homeassistant.components.homeassistant.async_get_system_info", + return_value={ + "installation_type": "Home Assistant Core", + "arch": arch, + }, + ), + patch( + "homeassistant.components.homeassistant._is_32_bit", + return_value=False, + ), ): assert await async_setup_component(hass, HOMEASSISTANT_DOMAIN, {}) await hass.async_block_till_done() @@ -731,28 +717,46 @@ async def test_deprecated_installation_issue_method( assert issue.severity == ir.IssueSeverity.WARNING assert issue.translation_placeholders == { "installation_type": "Core", - "arch": "generic-x86-64", + "arch": arch, } -async def test_deprecated_installation_issue_armv7_container( +@pytest.mark.parametrize( + "arch", + [ + "i386", + "armv7", + "armhf", + ], +) +async def test_deprecated_installation_issue_32bit( hass: HomeAssistant, issue_registry: ir.IssueRegistry, + arch: str, ) -> None: """Test deprecated installation issue.""" - with patch( - "homeassistant.components.homeassistant.async_get_system_info", - return_value={ - "installation_type": "Home Assistant Container", - "arch": "armv7", - }, + with ( + patch( + "homeassistant.components.homeassistant.async_get_system_info", + return_value={ + "installation_type": "Home Assistant Container", + "arch": arch, + }, + ), + patch( + "homeassistant.components.homeassistant._is_32_bit", + return_value=True, + ), + patch( + "homeassistant.components.homeassistant._get_arch", + return_value=arch, + ), ): assert await async_setup_component(hass, HOMEASSISTANT_DOMAIN, {}) await hass.async_block_till_done() assert len(issue_registry.issues) == 1 - issue = issue_registry.async_get_issue( - HOMEASSISTANT_DOMAIN, "deprecated_container_armv7" - ) + issue = issue_registry.async_get_issue(HOMEASSISTANT_DOMAIN, "deprecated_container") assert issue.domain == HOMEASSISTANT_DOMAIN assert issue.severity == ir.IssueSeverity.WARNING + assert issue.translation_placeholders == {"arch": arch} From dc4627f4136647503f70b4422f9b16d4202d00d8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 11 Jun 2025 18:07:37 +0000 Subject: [PATCH 197/266] Bump version to 2025.6.0b9 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 8c4fd0bf774..3992fc93730 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 = 6 -PATCH_VERSION: Final = "0b8" +PATCH_VERSION: Final = "0b9" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index 21717881d35..aa4b2a9c176 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.6.0b8" +version = "2025.6.0b9" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From cada2f84a9e719122ee0ab9893e07ca07ed84e2f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 11 Jun 2025 18:13:03 +0000 Subject: [PATCH 198/266] Hotfix ruff warnings --- tests/components/history_stats/test_init.py | 2 +- tests/components/homematicip_cloud/test_hap.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/history_stats/test_init.py b/tests/components/history_stats/test_init.py index 37b5416fdbb..f418b1f7ef1 100644 --- a/tests/components/history_stats/test_init.py +++ b/tests/components/history_stats/test_init.py @@ -69,7 +69,7 @@ def history_stats_config_entry( """Fixture to create a history_stats config entry.""" config_entry = MockConfigEntry( data={}, - domain=DOMAIN, + domain=HISTORY_STATS_DOMAIN, options={ CONF_NAME: DEFAULT_NAME, CONF_ENTITY_ID: sensor_entity_entry.entity_id, diff --git a/tests/components/homematicip_cloud/test_hap.py b/tests/components/homematicip_cloud/test_hap.py index 94d6f9d5dd6..c258c85ac93 100644 --- a/tests/components/homematicip_cloud/test_hap.py +++ b/tests/components/homematicip_cloud/test_hap.py @@ -237,7 +237,7 @@ async def test_get_state_after_disconnect( hass: HomeAssistant, hmip_config_entry: MockConfigEntry, simple_mock_home ) -> None: """Test get state after disconnect.""" - hass.config.components.add(DOMAIN) + hass.config.components.add(HMIPC_DOMAIN) hap = HomematicipHAP(hass, hmip_config_entry) assert hap From fb4c77d43b3e45e22cbd075d894459e9970e0587 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 11 Jun 2025 19:32:38 +0100 Subject: [PATCH 199/266] Add aiofiles to pyproject.toml (#146561) --- homeassistant/package_constraints.txt | 9 +-------- pyproject.toml | 1 + requirements.txt | 1 + script/gen_requirements_all.py | 8 -------- 4 files changed, 3 insertions(+), 16 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d0904cd12b3..57a037f0fb7 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -3,6 +3,7 @@ aiodhcpwatcher==1.2.0 aiodiscover==2.7.0 aiodns==3.4.0 +aiofiles==24.1.0 aiohasupervisor==0.3.1 aiohttp-asyncmdnsresolver==0.1.1 aiohttp-fast-zlib==0.3.0 @@ -201,14 +202,6 @@ tenacity!=8.4.0 # TypeError: 'Timeout' object does not support the context manager protocol async-timeout==4.0.3 -# aiofiles keeps getting downgraded by custom components -# causing newer methods to not be available and breaking -# some integrations at startup -# https://github.com/home-assistant/core/issues/127529 -# https://github.com/home-assistant/core/issues/122508 -# https://github.com/home-assistant/core/issues/118004 -aiofiles>=24.1.0 - # multidict < 6.4.0 has memory leaks # https://github.com/aio-libs/multidict/issues/1134 # https://github.com/aio-libs/multidict/issues/1131 diff --git a/pyproject.toml b/pyproject.toml index aa4b2a9c176..88bd59a95dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ classifiers = [ requires-python = ">=3.13.2" dependencies = [ "aiodns==3.4.0", + "aiofiles==24.1.0", # Integrations may depend on hassio integration without listing it to # change behavior based on presence of supervisor. Deprecated with #127228 # Lib can be removed with 2025.11 diff --git a/requirements.txt b/requirements.txt index e353adac9d3..6dc604d877b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,7 @@ # Home Assistant Core aiodns==3.4.0 +aiofiles==24.1.0 aiohasupervisor==0.3.1 aiohttp==3.12.12 aiohttp_cors==0.7.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 0ea69b365a2..8d1ce521b28 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -228,14 +228,6 @@ tenacity!=8.4.0 # TypeError: 'Timeout' object does not support the context manager protocol async-timeout==4.0.3 -# aiofiles keeps getting downgraded by custom components -# causing newer methods to not be available and breaking -# some integrations at startup -# https://github.com/home-assistant/core/issues/127529 -# https://github.com/home-assistant/core/issues/122508 -# https://github.com/home-assistant/core/issues/118004 -aiofiles>=24.1.0 - # multidict < 6.4.0 has memory leaks # https://github.com/aio-libs/multidict/issues/1134 # https://github.com/aio-libs/multidict/issues/1131 From 54d8d71de5c2d198ff5a71ba60c467a51e3259cb Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 11 Jun 2025 19:14:05 +0000 Subject: [PATCH 200/266] Bump version to 2025.6.0 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 3992fc93730..c006cd9dbed 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 = 6 -PATCH_VERSION: Final = "0b9" +PATCH_VERSION: Final = "0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index 88bd59a95dc..07f19628d0f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.6.0b9" +version = "2025.6.0" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From c3e3a36b4c8deeb59fb4c00ba110ee9f5b69938c Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Thu, 12 Jun 2025 22:32:04 +1000 Subject: [PATCH 201/266] Fix palette handling for LIFX Ceiling SKY effect (#146582) Signed-off-by: Avi Miller --- homeassistant/components/lifx/manager.py | 9 ++++++--- tests/components/lifx/test_light.py | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/lifx/manager.py b/homeassistant/components/lifx/manager.py index 887bc3c3527..9fae2628f1d 100644 --- a/homeassistant/components/lifx/manager.py +++ b/homeassistant/components/lifx/manager.py @@ -5,7 +5,7 @@ from __future__ import annotations import asyncio from collections.abc import Callable from datetime import timedelta -from typing import Any +from typing import TYPE_CHECKING, Any import aiolifx_effects from aiolifx_themes.painter import ThemePainter @@ -31,9 +31,12 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.service import async_extract_referenced_entity_ids from .const import _ATTR_COLOR_TEMP, ATTR_THEME, DATA_LIFX_MANAGER, DOMAIN -from .coordinator import LIFXUpdateCoordinator, Light +from .coordinator import LIFXUpdateCoordinator from .util import convert_8_to_16, find_hsbk +if TYPE_CHECKING: + from aiolifx.aiolifx import Light + SCAN_INTERVAL = timedelta(seconds=10) SERVICE_EFFECT_COLORLOOP = "effect_colorloop" @@ -426,8 +429,8 @@ class LIFXManager: ) -> None: """Start the firmware-based Sky effect.""" palette = kwargs.get(ATTR_PALETTE) + theme = Theme() if palette is not None: - theme = Theme() for hsbk in palette: theme.add_hsbk(hsbk[0], hsbk[1], hsbk[2], hsbk[3]) diff --git a/tests/components/lifx/test_light.py b/tests/components/lifx/test_light.py index 58843d63f9a..d66908c1b1a 100644 --- a/tests/components/lifx/test_light.py +++ b/tests/components/lifx/test_light.py @@ -843,7 +843,7 @@ async def test_sky_effect(hass: HomeAssistant) -> None: SERVICE_EFFECT_SKY, { ATTR_ENTITY_ID: entity_id, - ATTR_PALETTE: [], + ATTR_PALETTE: None, ATTR_SKY_TYPE: "Clouds", ATTR_CLOUD_SATURATION_MAX: 180, ATTR_CLOUD_SATURATION_MIN: 50, @@ -854,7 +854,7 @@ async def test_sky_effect(hass: HomeAssistant) -> None: bulb.power_level = 65535 bulb.effect = { "effect": "SKY", - "palette": [], + "palette": None, "sky_type": 2, "cloud_saturation_min": 50, "cloud_saturation_max": 180, From e7a88e99f922d027236ba5915aeeec20843c7523 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 12 Jun 2025 12:18:23 +0200 Subject: [PATCH 202/266] Fix fan is_on status in xiaomi_miio (#146592) --- homeassistant/components/xiaomi_miio/fan.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index c69bd150226..de2750f3c81 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -330,6 +330,12 @@ class XiaomiGenericDevice( """Return the percentage based speed of the fan.""" return None + @property + def is_on(self) -> bool | None: + """Return true if device is on.""" + # Base FanEntity uses percentage to determine if the device is on. + return self._attr_is_on + async def async_turn_on( self, percentage: int | None = None, From f0fc87e2b68222cee91f58f123eddce282759f5d Mon Sep 17 00:00:00 2001 From: Vasilis Valatsos Date: Fri, 13 Jun 2025 18:47:07 +0200 Subject: [PATCH 203/266] Drop HostKeyAlgorithms in aruba (#146619) --- homeassistant/components/aruba/device_tracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/aruba/device_tracker.py b/homeassistant/components/aruba/device_tracker.py index c2f0d44a6f8..667f2132fc8 100644 --- a/homeassistant/components/aruba/device_tracker.py +++ b/homeassistant/components/aruba/device_tracker.py @@ -89,7 +89,7 @@ class ArubaDeviceScanner(DeviceScanner): def get_aruba_data(self) -> dict[str, dict[str, str]] | None: """Retrieve data from Aruba Access Point and return parsed result.""" - connect = f"ssh {self.username}@{self.host} -o HostKeyAlgorithms=ssh-rsa" + connect = f"ssh {self.username}@{self.host}" ssh: pexpect.spawn[str] = pexpect.spawn(connect, encoding="utf-8") query = ssh.expect( [ From b2bb0aeb64d6eae68832872e5111da03ec56481e Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 12 Jun 2025 22:52:09 +0200 Subject: [PATCH 204/266] Update frontend to 20250531.3 (#146638) --- 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 4299d2b7503..efb4891debf 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==20250531.2"] + "requirements": ["home-assistant-frontend==20250531.3"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 57a037f0fb7..64f9d1649a6 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -39,7 +39,7 @@ habluetooth==3.49.0 hass-nabucasa==0.101.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250531.2 +home-assistant-frontend==20250531.3 home-assistant-intents==2025.6.10 httpx==0.28.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index b70c806c1be..43dbc527e51 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1164,7 +1164,7 @@ hole==0.8.0 holidays==0.74 # homeassistant.components.frontend -home-assistant-frontend==20250531.2 +home-assistant-frontend==20250531.3 # homeassistant.components.conversation home-assistant-intents==2025.6.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 39fe3779466..7f467760eaa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1010,7 +1010,7 @@ hole==0.8.0 holidays==0.74 # homeassistant.components.frontend -home-assistant-frontend==20250531.2 +home-assistant-frontend==20250531.3 # homeassistant.components.conversation home-assistant-intents==2025.6.10 From 52c62b31fd5e1fad2da63dad283fa3edef5fb8db Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Thu, 12 Jun 2025 17:24:10 +0300 Subject: [PATCH 205/266] Fix cookies with aiohttp >= 3.12.7 for Vodafone Station (#146647) --- homeassistant/components/vodafone_station/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/vodafone_station/utils.py b/homeassistant/components/vodafone_station/utils.py index 4f900412faf..faa498afdd6 100644 --- a/homeassistant/components/vodafone_station/utils.py +++ b/homeassistant/components/vodafone_station/utils.py @@ -9,5 +9,5 @@ from homeassistant.helpers import aiohttp_client async def async_client_session(hass: HomeAssistant) -> ClientSession: """Return a new aiohttp session.""" return aiohttp_client.async_create_clientsession( - hass, verify_ssl=False, cookie_jar=CookieJar(unsafe=True) + hass, verify_ssl=False, cookie_jar=CookieJar(unsafe=True, quote_cookie=False) ) From 5cd7ea06ad45334e3b1a10b90b19af8864500520 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 13 Jun 2025 15:58:44 +0200 Subject: [PATCH 206/266] Bump wakeonlan to 3.1.0 (#146655) Co-authored-by: J. Nick Koston --- homeassistant/components/samsungtv/entity.py | 4 ++-- homeassistant/components/samsungtv/manifest.json | 2 +- homeassistant/components/wake_on_lan/__init__.py | 2 +- homeassistant/components/wake_on_lan/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/samsungtv/entity.py b/homeassistant/components/samsungtv/entity.py index 1918f6ef28c..2927dcf2683 100644 --- a/homeassistant/components/samsungtv/entity.py +++ b/homeassistant/components/samsungtv/entity.py @@ -76,10 +76,10 @@ class SamsungTVEntity(CoordinatorEntity[SamsungTVDataUpdateCoordinator], Entity) def _wake_on_lan(self) -> None: """Wake the device via wake on lan.""" - send_magic_packet(self._mac, ip_address=self._host) + send_magic_packet(self._mac, ip_address=self._host) # type: ignore[arg-type] # If the ip address changed since we last saw the device # broadcast a packet as well - send_magic_packet(self._mac) + send_magic_packet(self._mac) # type: ignore[arg-type] async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index 5bb69e7f121..dc8133a1b1f 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -38,7 +38,7 @@ "getmac==0.9.5", "samsungctl[websocket]==0.7.1", "samsungtvws[async,encrypted]==2.7.2", - "wakeonlan==2.1.0", + "wakeonlan==3.1.0", "async-upnp-client==0.44.0" ], "ssdp": [ diff --git a/homeassistant/components/wake_on_lan/__init__.py b/homeassistant/components/wake_on_lan/__init__.py index d68d950e641..b2b2bac6480 100644 --- a/homeassistant/components/wake_on_lan/__init__.py +++ b/homeassistant/components/wake_on_lan/__init__.py @@ -52,7 +52,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) await hass.async_add_executor_job( - partial(wakeonlan.send_magic_packet, mac_address, **service_kwargs) + partial(wakeonlan.send_magic_packet, mac_address, **service_kwargs) # type: ignore[arg-type] ) hass.services.async_register( diff --git a/homeassistant/components/wake_on_lan/manifest.json b/homeassistant/components/wake_on_lan/manifest.json index c716a851ae4..34e9ccd5d21 100644 --- a/homeassistant/components/wake_on_lan/manifest.json +++ b/homeassistant/components/wake_on_lan/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wake_on_lan", "iot_class": "local_push", - "requirements": ["wakeonlan==2.1.0"] + "requirements": ["wakeonlan==3.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 43dbc527e51..2e0998a648f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3059,7 +3059,7 @@ vultr==0.1.2 # homeassistant.components.samsungtv # homeassistant.components.wake_on_lan -wakeonlan==2.1.0 +wakeonlan==3.1.0 # homeassistant.components.wallbox wallbox==0.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7f467760eaa..07e4bd250d7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2518,7 +2518,7 @@ vultr==0.1.2 # homeassistant.components.samsungtv # homeassistant.components.wake_on_lan -wakeonlan==2.1.0 +wakeonlan==3.1.0 # homeassistant.components.wallbox wallbox==0.9.0 From 7cf3116f5b3321695f721a8da9f4c26c62b028ea Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Thu, 12 Jun 2025 20:29:47 +0300 Subject: [PATCH 207/266] Bump hdate to 1.1.2 (#146659) --- homeassistant/components/jewish_calendar/manifest.json | 2 +- homeassistant/components/jewish_calendar/sensor.py | 10 +++------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/jewish_calendar/manifest.json b/homeassistant/components/jewish_calendar/manifest.json index 550a6514593..1ab967ecfa4 100644 --- a/homeassistant/components/jewish_calendar/manifest.json +++ b/homeassistant/components/jewish_calendar/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/jewish_calendar", "iot_class": "calculated", "loggers": ["hdate"], - "requirements": ["hdate[astral]==1.1.1"], + "requirements": ["hdate[astral]==1.1.2"], "single_config_entry": true } diff --git a/homeassistant/components/jewish_calendar/sensor.py b/homeassistant/components/jewish_calendar/sensor.py index cb38a3797eb..91c618e1c1c 100644 --- a/homeassistant/components/jewish_calendar/sensor.py +++ b/homeassistant/components/jewish_calendar/sensor.py @@ -73,7 +73,7 @@ INFO_SENSORS: tuple[JewishCalendarSensorDescription, ...] = ( translation_key="weekly_portion", device_class=SensorDeviceClass.ENUM, options_fn=lambda _: [str(p) for p in Parasha], - value_fn=lambda results: str(results.after_tzais_date.upcoming_shabbat.parasha), + value_fn=lambda results: results.after_tzais_date.upcoming_shabbat.parasha, ), JewishCalendarSensorDescription( key="holiday", @@ -98,17 +98,13 @@ INFO_SENSORS: tuple[JewishCalendarSensorDescription, ...] = ( key="omer_count", translation_key="omer_count", entity_registry_enabled_default=False, - value_fn=lambda results: ( - results.after_shkia_date.omer.total_days - if results.after_shkia_date.omer - else 0 - ), + value_fn=lambda results: results.after_shkia_date.omer.total_days, ), JewishCalendarSensorDescription( key="daf_yomi", translation_key="daf_yomi", entity_registry_enabled_default=False, - value_fn=lambda results: str(results.daytime_date.daf_yomi), + value_fn=lambda results: results.daytime_date.daf_yomi, ), ) diff --git a/requirements_all.txt b/requirements_all.txt index 2e0998a648f..8480beb7c66 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1133,7 +1133,7 @@ hass-splunk==0.1.1 hassil==2.2.3 # homeassistant.components.jewish_calendar -hdate[astral]==1.1.1 +hdate[astral]==1.1.2 # homeassistant.components.heatmiser heatmiserV3==2.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 07e4bd250d7..2704f0614b0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -988,7 +988,7 @@ hass-nabucasa==0.101.0 hassil==2.2.3 # homeassistant.components.jewish_calendar -hdate[astral]==1.1.1 +hdate[astral]==1.1.2 # homeassistant.components.here_travel_time here-routing==1.0.1 From e048a3da38d3be49edd3b4c87efcf98ae8c3d131 Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Thu, 12 Jun 2025 21:38:42 +0200 Subject: [PATCH 208/266] Bump linkplay to v0.2.12 (#146669) --- homeassistant/components/linkplay/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/linkplay/manifest.json b/homeassistant/components/linkplay/manifest.json index d6319c7a506..335f1acf396 100644 --- a/homeassistant/components/linkplay/manifest.json +++ b/homeassistant/components/linkplay/manifest.json @@ -7,6 +7,6 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["linkplay"], - "requirements": ["python-linkplay==0.2.11"], + "requirements": ["python-linkplay==0.2.12"], "zeroconf": ["_linkplay._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 8480beb7c66..b3d56a49e8c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2452,7 +2452,7 @@ python-juicenet==1.1.0 python-kasa[speedups]==0.10.2 # homeassistant.components.linkplay -python-linkplay==0.2.11 +python-linkplay==0.2.12 # homeassistant.components.lirc # python-lirc==1.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2704f0614b0..4732e5467ec 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2022,7 +2022,7 @@ python-juicenet==1.1.0 python-kasa[speedups]==0.10.2 # homeassistant.components.linkplay -python-linkplay==0.2.11 +python-linkplay==0.2.12 # homeassistant.components.lirc # python-lirc==1.2.3 From c2cf3482556ecc01758ab1f1b5c881c878ecf28c Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Fri, 13 Jun 2025 19:49:18 +0300 Subject: [PATCH 209/266] Filter speak notify entity for WHA devices in Alexa Devices (#146688) --- homeassistant/components/alexa_devices/notify.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/alexa_devices/notify.py b/homeassistant/components/alexa_devices/notify.py index ff0cd4e59ea..46db294377a 100644 --- a/homeassistant/components/alexa_devices/notify.py +++ b/homeassistant/components/alexa_devices/notify.py @@ -7,6 +7,7 @@ from dataclasses import dataclass from typing import Any, Final from aioamazondevices.api import AmazonDevice, AmazonEchoApi +from aioamazondevices.const import SPEAKER_GROUP_FAMILY from homeassistant.components.notify import NotifyEntity, NotifyEntityDescription from homeassistant.core import HomeAssistant @@ -22,6 +23,7 @@ PARALLEL_UPDATES = 1 class AmazonNotifyEntityDescription(NotifyEntityDescription): """Alexa Devices notify entity description.""" + is_supported: Callable[[AmazonDevice], bool] = lambda _device: True method: Callable[[AmazonEchoApi, AmazonDevice, str], Awaitable[None]] subkey: str @@ -31,6 +33,7 @@ NOTIFY: Final = ( key="speak", translation_key="speak", subkey="AUDIO_PLAYER", + is_supported=lambda _device: _device.device_family != SPEAKER_GROUP_FAMILY, method=lambda api, device, message: api.call_alexa_speak(device, message), ), AmazonNotifyEntityDescription( @@ -58,6 +61,7 @@ async def async_setup_entry( for sensor_desc in NOTIFY for serial_num in coordinator.data if sensor_desc.subkey in coordinator.data[serial_num].capabilities + and sensor_desc.is_supported(coordinator.data[serial_num]) ) From e81c8ce44d9f76656e85d512f2c4fa43a562d65c Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Fri, 13 Jun 2025 16:53:18 +0300 Subject: [PATCH 210/266] Bump aioamazondevices to 3.1.2 (#146690) --- homeassistant/components/alexa_devices/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/alexa_devices/conftest.py | 3 +++ 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/alexa_devices/manifest.json b/homeassistant/components/alexa_devices/manifest.json index 2a9e88cfd85..96f17d541fc 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.0.6"] + "requirements": ["aioamazondevices==3.1.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index b3d56a49e8c..bf71c75a83c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -182,7 +182,7 @@ aioairzone-cloud==0.6.12 aioairzone==1.0.0 # homeassistant.components.alexa_devices -aioamazondevices==3.0.6 +aioamazondevices==3.1.2 # homeassistant.components.ambient_network # homeassistant.components.ambient_station diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4732e5467ec..61e98df6960 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -170,7 +170,7 @@ aioairzone-cloud==0.6.12 aioairzone==1.0.0 # homeassistant.components.alexa_devices -aioamazondevices==3.0.6 +aioamazondevices==3.1.2 # 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 4ce2eb743ea..f1f40eebd27 100644 --- a/tests/components/alexa_devices/conftest.py +++ b/tests/components/alexa_devices/conftest.py @@ -56,6 +56,9 @@ def mock_amazon_devices_client() -> Generator[AsyncMock]: do_not_disturb=False, response_style=None, bluetooth_state=True, + entity_id="11111111-2222-3333-4444-555555555555", + appliance_id="G1234567890123456789012345678A", + sensors={}, ) } client.get_model_details = lambda device: DEVICE_TYPE_TO_MODEL.get( From 4ec711bd633a174f87aca2c59dfb9899e8ef94ba Mon Sep 17 00:00:00 2001 From: tronikos Date: Fri, 13 Jun 2025 06:58:27 -0700 Subject: [PATCH 211/266] Fix opower to work with aiohttp>=3.12.7 by disabling cookie quoting (#146697) Co-authored-by: J. Nick Koston --- homeassistant/components/opower/config_flow.py | 3 ++- homeassistant/components/opower/coordinator.py | 6 ++++-- homeassistant/components/opower/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 9 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/opower/config_flow.py b/homeassistant/components/opower/config_flow.py index 6396ba24a15..4753a77894e 100644 --- a/homeassistant/components/opower/config_flow.py +++ b/homeassistant/components/opower/config_flow.py @@ -10,6 +10,7 @@ from opower import ( CannotConnect, InvalidAuth, Opower, + create_cookie_jar, get_supported_utility_names, select_utility, ) @@ -39,7 +40,7 @@ async def _validate_login( ) -> dict[str, str]: """Validate login data and return any errors.""" api = Opower( - async_create_clientsession(hass), + async_create_clientsession(hass, cookie_jar=create_cookie_jar()), login_data[CONF_UTILITY], login_data[CONF_USERNAME], login_data[CONF_PASSWORD], diff --git a/homeassistant/components/opower/coordinator.py b/homeassistant/components/opower/coordinator.py index d03c30b7db0..189fa185cd1 100644 --- a/homeassistant/components/opower/coordinator.py +++ b/homeassistant/components/opower/coordinator.py @@ -12,6 +12,7 @@ from opower import ( MeterType, Opower, ReadResolution, + create_cookie_jar, ) from opower.exceptions import ApiException, CannotConnect, InvalidAuth @@ -30,7 +31,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, UnitOfEnergy, UnitOfVolume from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed -from homeassistant.helpers import aiohttp_client, issue_registry as ir +from homeassistant.helpers import issue_registry as ir +from homeassistant.helpers.aiohttp_client import async_create_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import dt as dt_util @@ -62,7 +64,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]): update_interval=timedelta(hours=12), ) self.api = Opower( - aiohttp_client.async_get_clientsession(hass), + async_create_clientsession(hass, cookie_jar=create_cookie_jar()), config_entry.data[CONF_UTILITY], config_entry.data[CONF_USERNAME], config_entry.data[CONF_PASSWORD], diff --git a/homeassistant/components/opower/manifest.json b/homeassistant/components/opower/manifest.json index 0aa26dbb4b1..4e88c5a68cc 100644 --- a/homeassistant/components/opower/manifest.json +++ b/homeassistant/components/opower/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/opower", "iot_class": "cloud_polling", "loggers": ["opower"], - "requirements": ["opower==0.12.3"] + "requirements": ["opower==0.12.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index bf71c75a83c..c54b30689ad 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1617,7 +1617,7 @@ openwrt-luci-rpc==1.1.17 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.12.3 +opower==0.12.4 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 61e98df6960..3a18bc52b4c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1370,7 +1370,7 @@ openhomedevice==2.2.0 openwebifpy==4.3.1 # homeassistant.components.opower -opower==0.12.3 +opower==0.12.4 # homeassistant.components.oralb oralb-ble==0.17.6 From cb74b2663f5f19f7741066a9e9a9355989ea8186 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Fri, 13 Jun 2025 03:46:01 -0700 Subject: [PATCH 212/266] Revert scan interval change in local calendar (#146700) --- homeassistant/components/local_calendar/calendar.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/homeassistant/components/local_calendar/calendar.py b/homeassistant/components/local_calendar/calendar.py index 8534cc1bfbf..639cf5234d1 100644 --- a/homeassistant/components/local_calendar/calendar.py +++ b/homeassistant/components/local_calendar/calendar.py @@ -36,11 +36,6 @@ _LOGGER = logging.getLogger(__name__) PRODID = "-//homeassistant.io//local_calendar 1.0//EN" -# The calendar on disk is only changed when this entity is updated, so there -# is no need to poll for changes. The calendar enttiy base class will handle -# refreshing the entity state based on the start or end time of the event. -SCAN_INTERVAL = timedelta(days=1) - async def async_setup_entry( hass: HomeAssistant, From d4ffeedc875afcf2b1270a80eb4ae7e4dd4b7701 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Fri, 13 Jun 2025 03:47:56 -0700 Subject: [PATCH 213/266] Partial revert of update to remote calendar to fix issue where calendar does not update (#146702) Partial revert --- .../components/remote_calendar/calendar.py | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/remote_calendar/calendar.py b/homeassistant/components/remote_calendar/calendar.py index 2f60918f010..f6918ea9706 100644 --- a/homeassistant/components/remote_calendar/calendar.py +++ b/homeassistant/components/remote_calendar/calendar.py @@ -4,6 +4,7 @@ from datetime import datetime import logging from ical.event import Event +from ical.timeline import Timeline from homeassistant.components.calendar import CalendarEntity, CalendarEvent from homeassistant.core import HomeAssistant @@ -48,12 +49,18 @@ class RemoteCalendarEntity( super().__init__(coordinator) self._attr_name = entry.data[CONF_CALENDAR_NAME] self._attr_unique_id = entry.entry_id - self._event: CalendarEvent | None = None + self._timeline: Timeline | None = None @property def event(self) -> CalendarEvent | None: """Return the next upcoming event.""" - return self._event + if self._timeline is None: + return None + now = dt_util.now() + events = self._timeline.active_after(now) + if event := next(events, None): + return _get_calendar_event(event) + return None async def async_get_events( self, hass: HomeAssistant, start_date: datetime, end_date: datetime @@ -79,15 +86,12 @@ class RemoteCalendarEntity( """ await super().async_update() - def next_timeline_event() -> CalendarEvent | None: + def _get_timeline() -> Timeline | None: """Return the next active event.""" now = dt_util.now() - events = self.coordinator.data.timeline_tz(now.tzinfo).active_after(now) - if event := next(events, None): - return _get_calendar_event(event) - return None + return self.coordinator.data.timeline_tz(now.tzinfo) - self._event = await self.hass.async_add_executor_job(next_timeline_event) + self._timeline = await self.hass.async_add_executor_job(_get_timeline) def _get_calendar_event(event: Event) -> CalendarEvent: From e89c3b1e929257b9c96c91f990a330292111a49b Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 13 Jun 2025 15:09:37 +0200 Subject: [PATCH 214/266] Ignore lingering pycares shutdown thread (#146733) --- tests/conftest.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index d13384055b1..3bacacc6c04 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -382,8 +382,10 @@ def verify_cleanup( # Verify no threads where left behind. threads = frozenset(threading.enumerate()) - threads_before for thread in threads: - assert isinstance(thread, threading._DummyThread) or thread.name.startswith( - "waitpid-" + assert ( + isinstance(thread, threading._DummyThread) + or thread.name.startswith("waitpid-") + or "_run_safe_shutdown_loop" in thread.name ) try: From a017d9415bd4d8ce02e1db5dfc791ff3a0dc336b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 13 Jun 2025 12:12:52 -0500 Subject: [PATCH 215/266] Bump aiodns to 3.5.0 (#146758) --- homeassistant/components/dnsip/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/dnsip/manifest.json b/homeassistant/components/dnsip/manifest.json index e004b386e02..6008fb83e1b 100644 --- a/homeassistant/components/dnsip/manifest.json +++ b/homeassistant/components/dnsip/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dnsip", "iot_class": "cloud_polling", - "requirements": ["aiodns==3.4.0"] + "requirements": ["aiodns==3.5.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 64f9d1649a6..ac33b8fe9f1 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -2,7 +2,7 @@ aiodhcpwatcher==1.2.0 aiodiscover==2.7.0 -aiodns==3.4.0 +aiodns==3.5.0 aiofiles==24.1.0 aiohasupervisor==0.3.1 aiohttp-asyncmdnsresolver==0.1.1 diff --git a/pyproject.toml b/pyproject.toml index 07f19628d0f..cdfd79cc764 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ classifiers = [ ] requires-python = ">=3.13.2" dependencies = [ - "aiodns==3.4.0", + "aiodns==3.5.0", "aiofiles==24.1.0", # Integrations may depend on hassio integration without listing it to # change behavior based on presence of supervisor. Deprecated with #127228 diff --git a/requirements.txt b/requirements.txt index 6dc604d877b..333198a5346 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ -c homeassistant/package_constraints.txt # Home Assistant Core -aiodns==3.4.0 +aiodns==3.5.0 aiofiles==24.1.0 aiohasupervisor==0.3.1 aiohttp==3.12.12 diff --git a/requirements_all.txt b/requirements_all.txt index c54b30689ad..d8cf0d2841c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -223,7 +223,7 @@ aiodhcpwatcher==1.2.0 aiodiscover==2.7.0 # homeassistant.components.dnsip -aiodns==3.4.0 +aiodns==3.5.0 # homeassistant.components.duke_energy aiodukeenergy==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3a18bc52b4c..7be1fcb1346 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -211,7 +211,7 @@ aiodhcpwatcher==1.2.0 aiodiscover==2.7.0 # homeassistant.components.dnsip -aiodns==3.4.0 +aiodns==3.5.0 # homeassistant.components.duke_energy aiodukeenergy==0.3.0 From df5f2531463754633d1348d9028bde0f335b48fe Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 13 Jun 2025 17:20:24 +0000 Subject: [PATCH 216/266] Bump version to 2025.6.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 c006cd9dbed..9d5ed69f8d7 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 = 6 -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 cdfd79cc764..067b310f371 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.6.0" +version = "2025.6.1" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From 2175754a1fcf827c908a3f4dd886ace47334a30c Mon Sep 17 00:00:00 2001 From: hahn-th <15319212+hahn-th@users.noreply.github.com> Date: Fri, 13 Jun 2025 19:57:03 +0200 Subject: [PATCH 217/266] Fix throttling issue in HomematicIP Cloud (#146683) Co-authored-by: J. Nick Koston --- .../components/homematicip_cloud/hap.py | 55 ++++++------------- .../homematicip_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/homematicip_cloud/test_hap.py | 21 ++++++- 5 files changed, 41 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index f3681a89110..c42ebff200d 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -128,6 +128,7 @@ class HomematicipHAP: self.config_entry.data.get(HMIPC_AUTHTOKEN), self.config_entry.data.get(HMIPC_NAME), ) + except HmipcConnectionError as err: raise ConfigEntryNotReady from err except Exception as err: # noqa: BLE001 @@ -210,41 +211,13 @@ class HomematicipHAP: for device in self.home.devices: device.fire_update_event() - async def async_connect(self) -> None: - """Start WebSocket connection.""" - tries = 0 - while True: - retry_delay = 2 ** min(tries, 8) + async def async_connect(self, home: AsyncHome) -> None: + """Connect to HomematicIP Cloud Websocket.""" + await home.enable_events() - try: - await self.home.get_current_state_async() - hmip_events = self.home.enable_events() - self.home.set_on_connected_handler(self.ws_connected_handler) - self.home.set_on_disconnected_handler(self.ws_disconnected_handler) - tries = 0 - await hmip_events - except HmipConnectionError: - _LOGGER.error( - ( - "Error connecting to HomematicIP with HAP %s. " - "Retrying in %d seconds" - ), - self.config_entry.unique_id, - retry_delay, - ) - - if self._ws_close_requested: - break - self._ws_close_requested = False - tries += 1 - - try: - self._retry_task = self.hass.async_create_task( - asyncio.sleep(retry_delay) - ) - await self._retry_task - except asyncio.CancelledError: - break + home.set_on_connected_handler(self.ws_connected_handler) + home.set_on_disconnected_handler(self.ws_disconnected_handler) + home.set_on_reconnect_handler(self.ws_reconnected_handler) async def async_reset(self) -> bool: """Close the websocket connection.""" @@ -272,14 +245,22 @@ class HomematicipHAP: async def ws_connected_handler(self) -> None: """Handle websocket connected.""" - _LOGGER.debug("WebSocket connection to HomematicIP established") + _LOGGER.info("Websocket connection to HomematicIP Cloud established") if self._ws_connection_closed.is_set(): await self.get_state() self._ws_connection_closed.clear() async def ws_disconnected_handler(self) -> None: """Handle websocket disconnection.""" - _LOGGER.warning("WebSocket connection to HomematicIP closed") + _LOGGER.warning("Websocket connection to HomematicIP Cloud closed") + self._ws_connection_closed.set() + + async def ws_reconnected_handler(self, reason: str) -> None: + """Handle websocket reconnection.""" + _LOGGER.info( + "Websocket connection to HomematicIP Cloud re-established due to reason: %s", + reason, + ) self._ws_connection_closed.set() async def get_hap( @@ -306,6 +287,6 @@ class HomematicipHAP: home.on_update(self.async_update) home.on_create(self.async_create_entity) - hass.loop.create_task(self.async_connect()) + await self.async_connect(home) return home diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index fc4a1cb831f..163f3c402dc 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.4"] + "requirements": ["homematicip==2.0.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index d8cf0d2841c..d0e8b10ce42 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1170,7 +1170,7 @@ home-assistant-frontend==20250531.3 home-assistant-intents==2025.6.10 # homeassistant.components.homematicip_cloud -homematicip==2.0.4 +homematicip==2.0.5 # homeassistant.components.horizon horimote==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7be1fcb1346..861c8097470 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1016,7 +1016,7 @@ home-assistant-frontend==20250531.3 home-assistant-intents==2025.6.10 # homeassistant.components.homematicip_cloud -homematicip==2.0.4 +homematicip==2.0.5 # homeassistant.components.remember_the_milk httplib2==0.20.4 diff --git a/tests/components/homematicip_cloud/test_hap.py b/tests/components/homematicip_cloud/test_hap.py index c258c85ac93..dec74fedc08 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 Mock, patch +from unittest.mock import AsyncMock, Mock, patch from homematicip.auth import Auth from homematicip.connection.connection_context import ConnectionContext @@ -16,6 +16,7 @@ from homeassistant.components.homematicip_cloud.const import ( ) from homeassistant.components.homematicip_cloud.errors import HmipcConnectionError from homeassistant.components.homematicip_cloud.hap import ( + AsyncHome, HomematicipAuth, HomematicipHAP, ) @@ -251,3 +252,21 @@ async def test_get_state_after_disconnect( assert hap._ws_connection_closed.is_set() await hap.ws_connected_handler() mock_get_state.assert_called_once() + + +async def test_async_connect( + hass: HomeAssistant, hmip_config_entry: MockConfigEntry, simple_mock_home +) -> None: + """Test async_connect.""" + hass.config.components.add(DOMAIN) + hap = HomematicipHAP(hass, hmip_config_entry) + assert hap + + simple_mock_home = AsyncMock(spec=AsyncHome, autospec=True) + + await hap.async_connect(simple_mock_home) + + simple_mock_home.set_on_connected_handler.assert_called_once() + simple_mock_home.set_on_disconnected_handler.assert_called_once() + simple_mock_home.set_on_reconnect_handler.assert_called_once() + simple_mock_home.enable_events.assert_called_once() From 25d1480f2a4b67e09f6040d58874de58b555af5a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 13 Jun 2025 18:09:05 +0000 Subject: [PATCH 218/266] Hotfix ruff warnings --- tests/components/homematicip_cloud/test_hap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/homematicip_cloud/test_hap.py b/tests/components/homematicip_cloud/test_hap.py index dec74fedc08..6d1e6024b68 100644 --- a/tests/components/homematicip_cloud/test_hap.py +++ b/tests/components/homematicip_cloud/test_hap.py @@ -258,7 +258,7 @@ async def test_async_connect( hass: HomeAssistant, hmip_config_entry: MockConfigEntry, simple_mock_home ) -> None: """Test async_connect.""" - hass.config.components.add(DOMAIN) + hass.config.components.add(HMIPC_DOMAIN) hap = HomematicipHAP(hass, hmip_config_entry) assert hap From 2ba9cb1510379ea1771f1f0599e23dfdd9b1cc01 Mon Sep 17 00:00:00 2001 From: Brian Rogers Date: Mon, 23 Jun 2025 04:31:35 -0400 Subject: [PATCH 219/266] Remove address info from Rachio calendar events (#145896) Co-authored-by: J. Nick Koston --- homeassistant/components/rachio/calendar.py | 5 ----- homeassistant/components/rachio/const.py | 2 -- 2 files changed, 7 deletions(-) diff --git a/homeassistant/components/rachio/calendar.py b/homeassistant/components/rachio/calendar.py index 18b1b6a4d8f..a8b593e1138 100644 --- a/homeassistant/components/rachio/calendar.py +++ b/homeassistant/components/rachio/calendar.py @@ -16,10 +16,8 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import dt as dt_util from .const import ( - KEY_ADDRESS, KEY_DURATION_SECONDS, KEY_ID, - KEY_LOCALITY, KEY_PROGRAM_ID, KEY_PROGRAM_NAME, KEY_RUN_SUMMARIES, @@ -65,7 +63,6 @@ class RachioCalendarEntity( super().__init__(coordinator) self.base_station = base_station self._event: CalendarEvent | None = None - self._location = coordinator.base_station[KEY_ADDRESS][KEY_LOCALITY] self._attr_translation_placeholders = { "base": coordinator.base_station[KEY_SERIAL_NUMBER] } @@ -87,7 +84,6 @@ class RachioCalendarEntity( end=dt_util.as_local(start_time) + timedelta(seconds=int(event[KEY_TOTAL_RUN_DURATION])), description=valves, - location=self._location, ) def _handle_upcoming_event(self) -> dict[str, Any] | None: @@ -155,7 +151,6 @@ class RachioCalendarEntity( start=event_start, end=event_end, description=valves, - location=self._location, uid=f"{run[KEY_PROGRAM_ID]}/{run[KEY_START_TIME]}", ) event_list.append(event) diff --git a/homeassistant/components/rachio/const.py b/homeassistant/components/rachio/const.py index 08a09f309f6..64b26526f57 100644 --- a/homeassistant/components/rachio/const.py +++ b/homeassistant/components/rachio/const.py @@ -75,8 +75,6 @@ KEY_PROGRAM_ID = "programId" KEY_PROGRAM_NAME = "programName" KEY_PROGRAM_RUN_SUMMARIES = "valveProgramRunSummaries" KEY_TOTAL_RUN_DURATION = "totalRunDurationSeconds" -KEY_ADDRESS = "address" -KEY_LOCALITY = "locality" KEY_SKIP = "skip" KEY_SKIPPABLE = "skippable" From 9ed6f226c64cad30feb0ede0fa229f4d188d58ea Mon Sep 17 00:00:00 2001 From: Raphael Hehl <7577984+RaHehl@users.noreply.github.com> Date: Sun, 8 Jun 2025 17:57:50 +0200 Subject: [PATCH 220/266] Bump uiprotect to 7.12.0 (#146337) --- 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 64bb278a8e2..713fe2f5248 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.11.0", "unifi-discovery==1.2.0"], + "requirements": ["uiprotect==7.12.0", "unifi-discovery==1.2.0"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index d0e8b10ce42..fd50d40f5bf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2987,7 +2987,7 @@ typedmonarchmoney==0.4.4 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==7.11.0 +uiprotect==7.12.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 861c8097470..f34958f8775 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2458,7 +2458,7 @@ typedmonarchmoney==0.4.4 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==7.11.0 +uiprotect==7.12.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From 83f26f7393a7f3a798d4f98a10a3f478ae361046 Mon Sep 17 00:00:00 2001 From: Raphael Hehl <7577984+RaHehl@users.noreply.github.com> Date: Tue, 10 Jun 2025 02:26:54 +0200 Subject: [PATCH 221/266] Bump uiprotect to 7.13.0 (#146410) --- 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 713fe2f5248..3c32935a995 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.12.0", "unifi-discovery==1.2.0"], + "requirements": ["uiprotect==7.13.0", "unifi-discovery==1.2.0"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index fd50d40f5bf..2e24bc14133 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2987,7 +2987,7 @@ typedmonarchmoney==0.4.4 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==7.12.0 +uiprotect==7.13.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f34958f8775..8f2236b00a5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2458,7 +2458,7 @@ typedmonarchmoney==0.4.4 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==7.12.0 +uiprotect==7.13.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From 0a8d1171293cdddd9c9bae54dccee1a86b97de4d Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 11 Jun 2025 23:52:31 +0200 Subject: [PATCH 222/266] Bump reolink-aio to 0.14.0 (#146566) --- homeassistant/components/reolink/entity.py | 2 +- homeassistant/components/reolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/reolink/conftest.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/reolink/entity.py b/homeassistant/components/reolink/entity.py index d7e8817b1b7..0d91670fc84 100644 --- a/homeassistant/components/reolink/entity.py +++ b/homeassistant/components/reolink/entity.py @@ -82,7 +82,7 @@ class ReolinkHostCoordinatorEntity(CoordinatorEntity[DataUpdateCoordinator[None] connections={(CONNECTION_NETWORK_MAC, self._host.api.mac_address)}, name=self._host.api.nvr_name, model=self._host.api.model, - model_id=self._host.api.item_number, + model_id=self._host.api.item_number(), manufacturer=self._host.api.manufacturer, hw_version=self._host.api.hardware_version, sw_version=self._host.api.sw_version, diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index 5ae8b0305e4..917ef9e73f7 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -19,5 +19,5 @@ "iot_class": "local_push", "loggers": ["reolink_aio"], "quality_scale": "platinum", - "requirements": ["reolink-aio==0.13.5"] + "requirements": ["reolink-aio==0.14.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2e24bc14133..5c84100f3e0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2652,7 +2652,7 @@ renault-api==0.3.1 renson-endura-delta==1.7.2 # homeassistant.components.reolink -reolink-aio==0.13.5 +reolink-aio==0.14.0 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8f2236b00a5..08182911a07 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2195,7 +2195,7 @@ renault-api==0.3.1 renson-endura-delta==1.7.2 # homeassistant.components.reolink -reolink-aio==0.13.5 +reolink-aio==0.14.0 # homeassistant.components.rflink rflink==0.0.66 diff --git a/tests/components/reolink/conftest.py b/tests/components/reolink/conftest.py index a2155ba00eb..1d8244a890a 100644 --- a/tests/components/reolink/conftest.py +++ b/tests/components/reolink/conftest.py @@ -99,7 +99,7 @@ def reolink_connect_class() -> Generator[MagicMock]: host_mock.sw_upload_progress.return_value = 100 host_mock.manufacturer = "Reolink" host_mock.model = TEST_HOST_MODEL - host_mock.item_number = TEST_ITEM_NUMBER + host_mock.item_number.return_value = TEST_ITEM_NUMBER host_mock.camera_model.return_value = TEST_CAM_MODEL host_mock.camera_name.return_value = TEST_NVR_NAME host_mock.camera_hardware_version.return_value = "IPC_00001" From 04b3227b9b880fc8cee4e7c16a3b2194708113e9 Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Thu, 12 Jun 2025 18:36:50 +0200 Subject: [PATCH 223/266] Bump pypck to 0.8.7 (#146657) --- homeassistant/components/lcn/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lcn/manifest.json b/homeassistant/components/lcn/manifest.json index be5d6299f09..9575c01515b 100644 --- a/homeassistant/components/lcn/manifest.json +++ b/homeassistant/components/lcn/manifest.json @@ -8,5 +8,5 @@ "documentation": "https://www.home-assistant.io/integrations/lcn", "iot_class": "local_push", "loggers": ["pypck"], - "requirements": ["pypck==0.8.6", "lcn-frontend==0.2.5"] + "requirements": ["pypck==0.8.7", "lcn-frontend==0.2.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5c84100f3e0..5220f415fa6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2236,7 +2236,7 @@ pypaperless==4.1.0 pypca==0.0.7 # homeassistant.components.lcn -pypck==0.8.6 +pypck==0.8.7 # homeassistant.components.pglab pypglab==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 08182911a07..f1cda7f651d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1857,7 +1857,7 @@ pypalazzetti==0.1.19 pypaperless==4.1.0 # homeassistant.components.lcn -pypck==0.8.6 +pypck==0.8.7 # homeassistant.components.pglab pypglab==0.0.5 From b249ae408fcf313369bdcb12f59b4335554b119b Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Sat, 14 Jun 2025 11:49:16 -0500 Subject: [PATCH 224/266] Update rokuecp to 0.19.5 (#146788) --- homeassistant/components/roku/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roku/manifest.json b/homeassistant/components/roku/manifest.json index 7fe2fb3b686..d5e2e2e5224 100644 --- a/homeassistant/components/roku/manifest.json +++ b/homeassistant/components/roku/manifest.json @@ -10,7 +10,7 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["rokuecp"], - "requirements": ["rokuecp==0.19.3"], + "requirements": ["rokuecp==0.19.5"], "ssdp": [ { "st": "roku:ecp", diff --git a/requirements_all.txt b/requirements_all.txt index 5220f415fa6..15204957762 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2673,7 +2673,7 @@ rjpl==0.3.6 rocketchat-API==0.6.1 # homeassistant.components.roku -rokuecp==0.19.3 +rokuecp==0.19.5 # homeassistant.components.romy romy==0.0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f1cda7f651d..6415238dcbd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2204,7 +2204,7 @@ rflink==0.0.66 ring-doorbell==0.9.13 # homeassistant.components.roku -rokuecp==0.19.3 +rokuecp==0.19.5 # homeassistant.components.romy romy==0.0.10 From 01a133a2b821bcb91cd75318df66000f31387dca Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 14 Jun 2025 19:53:51 +0200 Subject: [PATCH 225/266] Use Shelly main device area as suggested area for sub-devices (#146810) --- homeassistant/components/shelly/button.py | 8 +++++-- homeassistant/components/shelly/climate.py | 5 +++- .../components/shelly/coordinator.py | 12 +++++++++- homeassistant/components/shelly/entity.py | 24 +++++++++++++++---- homeassistant/components/shelly/event.py | 5 +++- homeassistant/components/shelly/sensor.py | 6 ++++- homeassistant/components/shelly/utils.py | 9 ++++++- 7 files changed, 57 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/shelly/button.py b/homeassistant/components/shelly/button.py index eab7514514d..ad03a373dba 100644 --- a/homeassistant/components/shelly/button.py +++ b/homeassistant/components/shelly/button.py @@ -235,11 +235,15 @@ class ShellyButton(ShellyBaseButton): self._attr_unique_id = f"{coordinator.mac}_{description.key}" if isinstance(coordinator, ShellyBlockCoordinator): self._attr_device_info = get_block_device_info( - coordinator.device, coordinator.mac + coordinator.device, + coordinator.mac, + suggested_area=coordinator.suggested_area, ) else: self._attr_device_info = get_rpc_device_info( - coordinator.device, coordinator.mac + coordinator.device, + coordinator.mac, + suggested_area=coordinator.suggested_area, ) self._attr_device_info = DeviceInfo( connections={(CONNECTION_NETWORK_MAC, coordinator.mac)} diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index 26fabe7e8b5..abc387f3efd 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -211,7 +211,10 @@ class BlockSleepingClimate( elif entry is not None: self._unique_id = entry.unique_id self._attr_device_info = get_block_device_info( - coordinator.device, coordinator.mac, sensor_block + coordinator.device, + coordinator.mac, + sensor_block, + suggested_area=coordinator.suggested_area, ) self._attr_name = get_block_entity_name( self.coordinator.device, sensor_block, None diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index f980ba8f914..cba559a9773 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -31,7 +31,11 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback -from homeassistant.helpers import device_registry as dr, issue_registry as ir +from homeassistant.helpers import ( + area_registry as ar, + device_registry as dr, + issue_registry as ir, +) from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -114,6 +118,7 @@ class ShellyCoordinatorBase[_DeviceT: BlockDevice | RpcDevice]( self.device = device self.device_id: str | None = None self._pending_platforms: list[Platform] | None = None + self.suggested_area: str | None = None device_name = device.name if device.initialized else entry.title interval_td = timedelta(seconds=update_interval) # The device has come online at least once. In the case of a sleeping RPC @@ -176,6 +181,11 @@ class ShellyCoordinatorBase[_DeviceT: BlockDevice | RpcDevice]( hw_version=f"gen{get_device_entry_gen(self.config_entry)}", configuration_url=f"http://{get_host(self.config_entry.data[CONF_HOST])}:{get_http_port(self.config_entry.data)}", ) + # We want to use the main device area as the suggested area for sub-devices. + if (area_id := device_entry.area_id) is not None: + area_registry = ar.async_get(self.hass) + if (area := area_registry.async_get_area(area_id)) is not None: + self.suggested_area = area.name self.device_id = device_entry.id async def shutdown(self) -> None: diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 1b0078890af..2c1678d56d9 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -362,7 +362,10 @@ class ShellyBlockEntity(CoordinatorEntity[ShellyBlockCoordinator]): self.block = block self._attr_name = get_block_entity_name(coordinator.device, block) self._attr_device_info = get_block_device_info( - coordinator.device, coordinator.mac, block + coordinator.device, + coordinator.mac, + block, + suggested_area=coordinator.suggested_area, ) self._attr_unique_id = f"{coordinator.mac}-{block.description}" @@ -405,7 +408,10 @@ class ShellyRpcEntity(CoordinatorEntity[ShellyRpcCoordinator]): super().__init__(coordinator) self.key = key self._attr_device_info = get_rpc_device_info( - coordinator.device, coordinator.mac, key + coordinator.device, + coordinator.mac, + key, + suggested_area=coordinator.suggested_area, ) self._attr_unique_id = f"{coordinator.mac}-{key}" self._attr_name = get_rpc_entity_name(coordinator.device, key) @@ -521,7 +527,9 @@ class ShellyRestAttributeEntity(CoordinatorEntity[ShellyBlockCoordinator]): ) self._attr_unique_id = f"{coordinator.mac}-{attribute}" self._attr_device_info = get_block_device_info( - coordinator.device, coordinator.mac + coordinator.device, + coordinator.mac, + suggested_area=coordinator.suggested_area, ) self._last_value = None @@ -630,7 +638,10 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity): self.entity_description = description self._attr_device_info = get_block_device_info( - coordinator.device, coordinator.mac, block + coordinator.device, + coordinator.mac, + block, + suggested_area=coordinator.suggested_area, ) if block is not None: @@ -698,7 +709,10 @@ class ShellySleepingRpcAttributeEntity(ShellyRpcAttributeEntity): self.entity_description = description self._attr_device_info = get_rpc_device_info( - coordinator.device, coordinator.mac, key + coordinator.device, + coordinator.mac, + key, + suggested_area=coordinator.suggested_area, ) self._attr_unique_id = self._attr_unique_id = ( f"{coordinator.mac}-{key}-{attribute}" diff --git a/homeassistant/components/shelly/event.py b/homeassistant/components/shelly/event.py index 677ea1f6138..2eb9ff00964 100644 --- a/homeassistant/components/shelly/event.py +++ b/homeassistant/components/shelly/event.py @@ -207,7 +207,10 @@ class ShellyRpcEvent(CoordinatorEntity[ShellyRpcCoordinator], EventEntity): super().__init__(coordinator) self.event_id = int(key.split(":")[-1]) self._attr_device_info = get_rpc_device_info( - coordinator.device, coordinator.mac, key + coordinator.device, + coordinator.mac, + key, + suggested_area=coordinator.suggested_area, ) self._attr_unique_id = f"{coordinator.mac}-{key}" self._attr_name = get_rpc_entity_name(coordinator.device, key) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 0ea246c7734..3a6f5f221c5 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -139,7 +139,11 @@ class RpcEmeterPhaseSensor(RpcSensor): super().__init__(coordinator, key, attribute, description) self._attr_device_info = get_rpc_device_info( - coordinator.device, coordinator.mac, key, description.emeter_phase + coordinator.device, + coordinator.mac, + key, + emeter_phase=description.emeter_phase, + suggested_area=coordinator.suggested_area, ) diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index cc0f2cf75d5..953fcbace06 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -751,6 +751,7 @@ def get_rpc_device_info( mac: str, key: str | None = None, emeter_phase: str | None = None, + suggested_area: str | None = None, ) -> DeviceInfo: """Return device info for RPC device.""" if key is None: @@ -770,6 +771,7 @@ def get_rpc_device_info( identifiers={(DOMAIN, f"{mac}-{key}-{emeter_phase.lower()}")}, name=get_rpc_sub_device_name(device, key, emeter_phase), manufacturer="Shelly", + suggested_area=suggested_area, via_device=(DOMAIN, mac), ) @@ -784,6 +786,7 @@ def get_rpc_device_info( identifiers={(DOMAIN, f"{mac}-{key}")}, name=get_rpc_sub_device_name(device, key), manufacturer="Shelly", + suggested_area=suggested_area, via_device=(DOMAIN, mac), ) @@ -805,7 +808,10 @@ def get_blu_trv_device_info( def get_block_device_info( - device: BlockDevice, mac: str, block: Block | None = None + device: BlockDevice, + mac: str, + block: Block | None = None, + suggested_area: str | None = None, ) -> DeviceInfo: """Return device info for Block device.""" if ( @@ -820,6 +826,7 @@ def get_block_device_info( identifiers={(DOMAIN, f"{mac}-{block.description}")}, name=get_block_sub_device_name(device, block), manufacturer="Shelly", + suggested_area=suggested_area, via_device=(DOMAIN, mac), ) From d684360ebd1fc148823d5985f2b9485508055746 Mon Sep 17 00:00:00 2001 From: elmurato <1382097+elmurato@users.noreply.github.com> Date: Mon, 16 Jun 2025 22:46:11 +0200 Subject: [PATCH 226/266] Fix blocking open in Minecraft Server (#146820) Fix blocking open by dnspython --- .../components/minecraft_server/__init__.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/minecraft_server/__init__.py b/homeassistant/components/minecraft_server/__init__.py index d8f60380a6c..e74b78446e5 100644 --- a/homeassistant/components/minecraft_server/__init__.py +++ b/homeassistant/components/minecraft_server/__init__.py @@ -5,6 +5,7 @@ from __future__ import annotations import logging from typing import Any +import dns.asyncresolver import dns.rdata import dns.rdataclass import dns.rdatatype @@ -22,20 +23,23 @@ PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] _LOGGER = logging.getLogger(__name__) -def load_dnspython_rdata_classes() -> None: - """Load dnspython rdata classes used by mcstatus.""" +def prevent_dnspython_blocking_operations() -> None: + """Prevent dnspython blocking operations by pre-loading required data.""" + + # Blocking import: https://github.com/rthalley/dnspython/issues/1083 for rdtype in dns.rdatatype.RdataType: if not dns.rdatatype.is_metatype(rdtype) or rdtype == dns.rdatatype.OPT: dns.rdata.get_rdata_class(dns.rdataclass.IN, rdtype) # type: ignore[no-untyped-call] + # Blocking open: https://github.com/rthalley/dnspython/issues/1200 + dns.asyncresolver.get_default_resolver() + async def async_setup_entry( hass: HomeAssistant, entry: MinecraftServerConfigEntry ) -> bool: """Set up Minecraft Server from a config entry.""" - - # Workaround to avoid blocking imports from dnspython (https://github.com/rthalley/dnspython/issues/1083) - await hass.async_add_executor_job(load_dnspython_rdata_classes) + await hass.async_add_executor_job(prevent_dnspython_blocking_operations) # Create coordinator instance and store it. coordinator = MinecraftServerCoordinator(hass, entry) From a6e6b6db5adbcdd1ab009bc5e1907f1c651341c4 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sun, 15 Jun 2025 01:45:28 +0300 Subject: [PATCH 227/266] Bump aioamazondevices to 3.1.3 (#146828) --- 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 96f17d541fc..0ea4c32a75e 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.1.2"] + "requirements": ["aioamazondevices==3.1.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 15204957762..253b7959c46 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -182,7 +182,7 @@ aioairzone-cloud==0.6.12 aioairzone==1.0.0 # homeassistant.components.alexa_devices -aioamazondevices==3.1.2 +aioamazondevices==3.1.3 # homeassistant.components.ambient_network # homeassistant.components.ambient_station diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6415238dcbd..190f257486b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -170,7 +170,7 @@ aioairzone-cloud==0.6.12 aioairzone==1.0.0 # homeassistant.components.alexa_devices -aioamazondevices==3.1.2 +aioamazondevices==3.1.3 # homeassistant.components.ambient_network # homeassistant.components.ambient_station From 8e685b16264f325e9bbe11e4d1fd00bc1c457e50 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 14 Jun 2025 11:13:20 -0500 Subject: [PATCH 228/266] Bump aiohttp to 3.12.13 (#146830) changelog: https://github.com/aio-libs/aiohttp/compare/v3.12.12...v3.12.13 Likely does not affect us at all but just in case, tagging --- 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 ac33b8fe9f1..706218022b3 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -7,7 +7,7 @@ aiofiles==24.1.0 aiohasupervisor==0.3.1 aiohttp-asyncmdnsresolver==0.1.1 aiohttp-fast-zlib==0.3.0 -aiohttp==3.12.12 +aiohttp==3.12.13 aiohttp_cors==0.7.0 aiousbwatcher==1.1.1 aiozoneinfo==0.2.3 diff --git a/pyproject.toml b/pyproject.toml index 067b310f371..226420c7605 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,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.12", + "aiohttp==3.12.13", "aiohttp_cors==0.7.0", "aiohttp-fast-zlib==0.3.0", "aiohttp-asyncmdnsresolver==0.1.1", diff --git a/requirements.txt b/requirements.txt index 333198a5346..70f214bbfc5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ aiodns==3.5.0 aiofiles==24.1.0 aiohasupervisor==0.3.1 -aiohttp==3.12.12 +aiohttp==3.12.13 aiohttp_cors==0.7.0 aiohttp-fast-zlib==0.3.0 aiohttp-asyncmdnsresolver==0.1.1 From 05831493e217362ef8edc8f80fa37f2f1e3549bf Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 14 Jun 2025 19:01:41 +0200 Subject: [PATCH 229/266] Bump motion blinds to 0.6.28 (#146831) --- homeassistant/components/motion_blinds/cover.py | 1 + homeassistant/components/motion_blinds/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/motion_blinds/cover.py b/homeassistant/components/motion_blinds/cover.py index 165c4c19675..9cff2956a5f 100644 --- a/homeassistant/components/motion_blinds/cover.py +++ b/homeassistant/components/motion_blinds/cover.py @@ -62,6 +62,7 @@ TILT_DEVICE_MAP = { BlindType.VerticalBlind: CoverDeviceClass.BLIND, BlindType.VerticalBlindLeft: CoverDeviceClass.BLIND, BlindType.VerticalBlindRight: CoverDeviceClass.BLIND, + BlindType.RollerTiltMotor: CoverDeviceClass.BLIND, } TILT_ONLY_DEVICE_MAP = { diff --git a/homeassistant/components/motion_blinds/manifest.json b/homeassistant/components/motion_blinds/manifest.json index 1a6c9c5f82f..a82da20396f 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.27"] + "requirements": ["motionblinds==0.6.28"] } diff --git a/requirements_all.txt b/requirements_all.txt index 253b7959c46..b8fc088a725 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1448,7 +1448,7 @@ monzopy==1.4.2 mopeka-iot-ble==0.8.0 # homeassistant.components.motion_blinds -motionblinds==0.6.27 +motionblinds==0.6.28 # homeassistant.components.motionblinds_ble motionblindsble==0.1.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 190f257486b..17f45b98e10 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1237,7 +1237,7 @@ monzopy==1.4.2 mopeka-iot-ble==0.8.0 # homeassistant.components.motion_blinds -motionblinds==0.6.27 +motionblinds==0.6.28 # homeassistant.components.motionblinds_ble motionblindsble==0.1.3 From a7b2f800f857ab8f276d191e187f928d2edc2c05 Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Sun, 15 Jun 2025 19:32:13 +0200 Subject: [PATCH 230/266] Bump pypck to 0.8.8 (#146841) --- homeassistant/components/lcn/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lcn/manifest.json b/homeassistant/components/lcn/manifest.json index 9575c01515b..30584bc33f6 100644 --- a/homeassistant/components/lcn/manifest.json +++ b/homeassistant/components/lcn/manifest.json @@ -8,5 +8,5 @@ "documentation": "https://www.home-assistant.io/integrations/lcn", "iot_class": "local_push", "loggers": ["pypck"], - "requirements": ["pypck==0.8.7", "lcn-frontend==0.2.5"] + "requirements": ["pypck==0.8.8", "lcn-frontend==0.2.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index b8fc088a725..943708399a5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2236,7 +2236,7 @@ pypaperless==4.1.0 pypca==0.0.7 # homeassistant.components.lcn -pypck==0.8.7 +pypck==0.8.8 # homeassistant.components.pglab pypglab==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 17f45b98e10..34dc54576bb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1857,7 +1857,7 @@ pypalazzetti==0.1.19 pypaperless==4.1.0 # homeassistant.components.lcn -pypck==0.8.7 +pypck==0.8.8 # homeassistant.components.pglab pypglab==0.0.5 From da9775615705eed2b5a12bc9f9e25b35fbc7e965 Mon Sep 17 00:00:00 2001 From: Hessel Date: Mon, 16 Jun 2025 15:15:17 +0200 Subject: [PATCH 231/266] Fix missing key for ecosmart in older Wallbox models (#146847) * fix 146839, missing key * added tests for this issue * added tests for this issue * added tests for this issue, formatting * Prevent loading select on missing key * Prevent loading select on missing key - formatting fixed * Update homeassistant/components/wallbox/coordinator.py Co-authored-by: Joost Lekkerkerker --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/wallbox/const.py | 1 + .../components/wallbox/coordinator.py | 21 ++++++++++------ homeassistant/components/wallbox/select.py | 16 ++++++------ tests/components/wallbox/__init__.py | 25 +++++++++++++++++++ tests/components/wallbox/test_init.py | 13 ++++++++++ 5 files changed, 61 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/wallbox/const.py b/homeassistant/components/wallbox/const.py index d978e1ec7c9..5aa659a0527 100644 --- a/homeassistant/components/wallbox/const.py +++ b/homeassistant/components/wallbox/const.py @@ -74,3 +74,4 @@ class EcoSmartMode(StrEnum): OFF = "off" ECO_MODE = "eco_mode" FULL_SOLAR = "full_solar" + DISABLED = "disabled" diff --git a/homeassistant/components/wallbox/coordinator.py b/homeassistant/components/wallbox/coordinator.py index 60f062e57cc..8276ee14eaf 100644 --- a/homeassistant/components/wallbox/coordinator.py +++ b/homeassistant/components/wallbox/coordinator.py @@ -166,13 +166,20 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]): ) # Set current solar charging mode - eco_smart_enabled = data[CHARGER_DATA_KEY][CHARGER_ECO_SMART_KEY][ - CHARGER_ECO_SMART_STATUS_KEY - ] - eco_smart_mode = data[CHARGER_DATA_KEY][CHARGER_ECO_SMART_KEY][ - CHARGER_ECO_SMART_MODE_KEY - ] - if eco_smart_enabled is False: + eco_smart_enabled = ( + data[CHARGER_DATA_KEY] + .get(CHARGER_ECO_SMART_KEY, {}) + .get(CHARGER_ECO_SMART_STATUS_KEY) + ) + + eco_smart_mode = ( + data[CHARGER_DATA_KEY] + .get(CHARGER_ECO_SMART_KEY, {}) + .get(CHARGER_ECO_SMART_MODE_KEY) + ) + if eco_smart_mode is None: + data[CHARGER_ECO_SMART_KEY] = EcoSmartMode.DISABLED + elif eco_smart_enabled is False: data[CHARGER_ECO_SMART_KEY] = EcoSmartMode.OFF elif eco_smart_mode == 0: data[CHARGER_ECO_SMART_KEY] = EcoSmartMode.ECO_MODE diff --git a/homeassistant/components/wallbox/select.py b/homeassistant/components/wallbox/select.py index 7ad7a135bc8..0048aa35c7c 100644 --- a/homeassistant/components/wallbox/select.py +++ b/homeassistant/components/wallbox/select.py @@ -63,15 +63,15 @@ async def async_setup_entry( ) -> None: """Create wallbox select entities in HASS.""" coordinator: WallboxCoordinator = hass.data[DOMAIN][entry.entry_id] - - async_add_entities( - WallboxSelect(coordinator, description) - for ent in coordinator.data - if ( - (description := SELECT_TYPES.get(ent)) - and description.supported_fn(coordinator) + if coordinator.data[CHARGER_ECO_SMART_KEY] != EcoSmartMode.DISABLED: + async_add_entities( + WallboxSelect(coordinator, description) + for ent in coordinator.data + if ( + (description := SELECT_TYPES.get(ent)) + and description.supported_fn(coordinator) + ) ) - ) class WallboxSelect(WallboxEntity, SelectEntity): diff --git a/tests/components/wallbox/__init__.py b/tests/components/wallbox/__init__.py index d347777f7e8..83e39d2f602 100644 --- a/tests/components/wallbox/__init__.py +++ b/tests/components/wallbox/__init__.py @@ -216,6 +216,31 @@ async def setup_integration(hass: HomeAssistant, entry: MockConfigEntry) -> None await hass.async_block_till_done() +async def setup_integration_no_eco_mode( + hass: HomeAssistant, entry: MockConfigEntry +) -> None: + """Test wallbox sensor class setup.""" + with requests_mock.Mocker() as mock_request: + mock_request.get( + "https://user-api.wall-box.com/users/signin", + json=authorisation_response, + status_code=HTTPStatus.OK, + ) + mock_request.get( + "https://api.wall-box.com/chargers/status/12345", + json=test_response_no_power_boost, + status_code=HTTPStatus.OK, + ) + mock_request.put( + "https://api.wall-box.com/v2/charger/12345", + json={CHARGER_MAX_CHARGING_CURRENT_KEY: 20}, + status_code=HTTPStatus.OK, + ) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + async def setup_integration_select( hass: HomeAssistant, entry: MockConfigEntry, response ) -> None: diff --git a/tests/components/wallbox/test_init.py b/tests/components/wallbox/test_init.py index b4b5a199243..6d6a5cd1417 100644 --- a/tests/components/wallbox/test_init.py +++ b/tests/components/wallbox/test_init.py @@ -13,6 +13,7 @@ from . import ( authorisation_response, setup_integration, setup_integration_connection_error, + setup_integration_no_eco_mode, setup_integration_read_only, test_response, ) @@ -138,3 +139,15 @@ async def test_wallbox_refresh_failed_read_only( assert await hass.config_entries.async_unload(entry.entry_id) assert entry.state is ConfigEntryState.NOT_LOADED + + +async def test_wallbox_setup_load_entry_no_eco_mode( + hass: HomeAssistant, entry: MockConfigEntry +) -> None: + """Test Wallbox Unload.""" + + await setup_integration_no_eco_mode(hass, entry) + assert entry.state is ConfigEntryState.LOADED + + assert await hass.config_entries.async_unload(entry.entry_id) + assert entry.state is ConfigEntryState.NOT_LOADED From d66dee54112c84e8f9d4918bef5ce2f6e940a4b0 Mon Sep 17 00:00:00 2001 From: Ernst Klamer Date: Mon, 16 Jun 2025 10:29:00 +0200 Subject: [PATCH 232/266] Bump bthome-ble to 3.13.1 (#146871) --- homeassistant/components/bthome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bthome/manifest.json b/homeassistant/components/bthome/manifest.json index 4130606ff5c..0bbdfae50e4 100644 --- a/homeassistant/components/bthome/manifest.json +++ b/homeassistant/components/bthome/manifest.json @@ -20,5 +20,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/bthome", "iot_class": "local_push", - "requirements": ["bthome-ble==3.12.4"] + "requirements": ["bthome-ble==3.13.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 943708399a5..e5a1e59a501 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -683,7 +683,7 @@ brunt==1.2.0 bt-proximity==0.2.1 # homeassistant.components.bthome -bthome-ble==3.12.4 +bthome-ble==3.13.1 # homeassistant.components.bt_home_hub_5 bthomehub5-devicelist==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 34dc54576bb..431fedfe1ea 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -607,7 +607,7 @@ brottsplatskartan==1.0.5 brunt==1.2.0 # homeassistant.components.bthome -bthome-ble==3.12.4 +bthome-ble==3.13.1 # homeassistant.components.buienradar buienradar==1.0.6 From 9b744e2fef13f511abc24f186603a66b427157cb Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sun, 15 Jun 2025 20:53:32 +0200 Subject: [PATCH 233/266] Bump reolink-aio to 0.14.1 (#146903) --- 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 917ef9e73f7..04996689bf7 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.0"] + "requirements": ["reolink-aio==0.14.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index e5a1e59a501..7bf44aa7d91 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2652,7 +2652,7 @@ renault-api==0.3.1 renson-endura-delta==1.7.2 # homeassistant.components.reolink -reolink-aio==0.14.0 +reolink-aio==0.14.1 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 431fedfe1ea..3107b7a898a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2195,7 +2195,7 @@ renault-api==0.3.1 renson-endura-delta==1.7.2 # homeassistant.components.reolink -reolink-aio==0.14.0 +reolink-aio==0.14.1 # homeassistant.components.rflink rflink==0.0.66 From d0060a2b213f1622cd4a7defbbb549c8ebf024a4 Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Mon, 16 Jun 2025 14:17:36 +0200 Subject: [PATCH 234/266] Add debug log for update in onedrive (#146907) --- homeassistant/components/onedrive/coordinator.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/onedrive/coordinator.py b/homeassistant/components/onedrive/coordinator.py index 3eb7d762712..ff05b19f84d 100644 --- a/homeassistant/components/onedrive/coordinator.py +++ b/homeassistant/components/onedrive/coordinator.py @@ -66,6 +66,7 @@ class OneDriveUpdateCoordinator(DataUpdateCoordinator[Drive]): translation_domain=DOMAIN, translation_key="authentication_failed" ) from err except OneDriveException as err: + _LOGGER.debug("Failed to fetch drive data: %s") raise UpdateFailed( translation_domain=DOMAIN, translation_key="update_failed" ) from err From 94a2642ce9579017102b81e5d6876d40446a574c Mon Sep 17 00:00:00 2001 From: Ravaka Razafimanantsoa <3774520+SeraphicRav@users.noreply.github.com> Date: Sun, 22 Jun 2025 21:12:09 +0900 Subject: [PATCH 235/266] Switchbot Cloud: Fix device type filtering in sensor (#146945) * Add Smart Lock Ultra support and fix device type filtering in sensor integration * Adding fix in binary sensor * Fix --------- Co-authored-by: Joostlek --- .../switchbot_cloud/binary_sensor.py | 1 + .../components/switchbot_cloud/sensor.py | 1 + .../switchbot_cloud/test_binary_sensor.py | 39 +++++++++++++++++++ .../components/switchbot_cloud/test_sensor.py | 26 +++++++++++++ 4 files changed, 67 insertions(+) create mode 100644 tests/components/switchbot_cloud/test_binary_sensor.py diff --git a/homeassistant/components/switchbot_cloud/binary_sensor.py b/homeassistant/components/switchbot_cloud/binary_sensor.py index 14278072c83..752c428fa6c 100644 --- a/homeassistant/components/switchbot_cloud/binary_sensor.py +++ b/homeassistant/components/switchbot_cloud/binary_sensor.py @@ -69,6 +69,7 @@ async def async_setup_entry( for description in BINARY_SENSOR_DESCRIPTIONS_BY_DEVICE_TYPES[ device.device_type ] + if device.device_type in BINARY_SENSOR_DESCRIPTIONS_BY_DEVICE_TYPES ) diff --git a/homeassistant/components/switchbot_cloud/sensor.py b/homeassistant/components/switchbot_cloud/sensor.py index 9975bd49186..9920717a8d7 100644 --- a/homeassistant/components/switchbot_cloud/sensor.py +++ b/homeassistant/components/switchbot_cloud/sensor.py @@ -151,6 +151,7 @@ async def async_setup_entry( SwitchBotCloudSensor(data.api, device, coordinator, description) for device, coordinator in data.devices.sensors for description in SENSOR_DESCRIPTIONS_BY_DEVICE_TYPES[device.device_type] + if device.device_type in SENSOR_DESCRIPTIONS_BY_DEVICE_TYPES ) diff --git a/tests/components/switchbot_cloud/test_binary_sensor.py b/tests/components/switchbot_cloud/test_binary_sensor.py new file mode 100644 index 00000000000..753653af9a8 --- /dev/null +++ b/tests/components/switchbot_cloud/test_binary_sensor.py @@ -0,0 +1,39 @@ +"""Test for the switchbot_cloud binary sensors.""" + +from unittest.mock import patch + +from switchbot_api import Device + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import configure_integration + + +async def test_unsupported_device_type( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_list_devices, + mock_get_status, +) -> None: + """Test that unsupported device types do not create sensors.""" + mock_list_devices.return_value = [ + Device( + version="V1.0", + deviceId="unsupported-id-1", + deviceName="unsupported-device", + deviceType="UnsupportedDevice", + hubDeviceId="test-hub-id", + ), + ] + mock_get_status.return_value = {} + + with patch( + "homeassistant.components.switchbot_cloud.PLATFORMS", [Platform.BINARY_SENSOR] + ): + entry = await configure_integration(hass) + + # Assert no binary sensor entities were created for unsupported device type + entities = er.async_entries_for_config_entry(entity_registry, entry.entry_id) + assert len([e for e in entities if e.domain == "binary_sensor"]) == 0 diff --git a/tests/components/switchbot_cloud/test_sensor.py b/tests/components/switchbot_cloud/test_sensor.py index 0927e3cf1ea..ccad2f1ad6c 100644 --- a/tests/components/switchbot_cloud/test_sensor.py +++ b/tests/components/switchbot_cloud/test_sensor.py @@ -65,3 +65,29 @@ async def test_meter_no_coordinator_data( entry = await configure_integration(hass) await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) + + +async def test_unsupported_device_type( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_list_devices, + mock_get_status, +) -> None: + """Test that unsupported device types do not create sensors.""" + mock_list_devices.return_value = [ + Device( + version="V1.0", + deviceId="unsupported-id-1", + deviceName="unsupported-device", + deviceType="UnsupportedDevice", + hubDeviceId="test-hub-id", + ), + ] + mock_get_status.return_value = {} + + with patch("homeassistant.components.switchbot_cloud.PLATFORMS", [Platform.SENSOR]): + entry = await configure_integration(hass) + + # Assert no sensor entities were created for unsupported device type + entities = er.async_entries_for_config_entry(entity_registry, entry.entry_id) + assert len([e for e in entities if e.domain == "sensor"]) == 0 From d2d5b29e2bbfd1203d50e9646b1384844c65ceab Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 16 Jun 2025 23:44:14 +0200 Subject: [PATCH 236/266] Bump pySmartThings to 3.2.5 (#146983) --- 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 481048c3bdb..7c3fc47e512 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.4"] + "requirements": ["pysmartthings==3.2.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index 7bf44aa7d91..fbc8c403f9c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2341,7 +2341,7 @@ pysmappee==0.2.29 pysmarlaapi==0.8.2 # homeassistant.components.smartthings -pysmartthings==3.2.4 +pysmartthings==3.2.5 # homeassistant.components.smarty pysmarty2==0.10.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3107b7a898a..5659daa20ee 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1941,7 +1941,7 @@ pysmappee==0.2.29 pysmarlaapi==0.8.2 # homeassistant.components.smartthings -pysmartthings==3.2.4 +pysmartthings==3.2.5 # homeassistant.components.smarty pysmarty2==0.10.2 From 8f13520a1ce9c734533a3e77fe1432ee3172a9f5 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 17 Jun 2025 07:06:51 -0700 Subject: [PATCH 237/266] Bump ical to 10.0.4 (#147005) * Bump ical to 10.0.4 * Bump ical to 10.0.4 in google --- homeassistant/components/google/manifest.json | 2 +- homeassistant/components/local_calendar/manifest.json | 2 +- homeassistant/components/local_todo/manifest.json | 2 +- homeassistant/components/remote_calendar/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index fecd245869a..1acfa3a2ad1 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/google", "iot_class": "cloud_polling", "loggers": ["googleapiclient"], - "requirements": ["gcal-sync==7.1.0", "oauth2client==4.1.3", "ical==10.0.0"] + "requirements": ["gcal-sync==7.1.0", "oauth2client==4.1.3", "ical==10.0.4"] } diff --git a/homeassistant/components/local_calendar/manifest.json b/homeassistant/components/local_calendar/manifest.json index e0b08313d63..3bf00f30624 100644 --- a/homeassistant/components/local_calendar/manifest.json +++ b/homeassistant/components/local_calendar/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/local_calendar", "iot_class": "local_polling", "loggers": ["ical"], - "requirements": ["ical==10.0.0"] + "requirements": ["ical==10.0.4"] } diff --git a/homeassistant/components/local_todo/manifest.json b/homeassistant/components/local_todo/manifest.json index c8e80e4f91b..134cea5293b 100644 --- a/homeassistant/components/local_todo/manifest.json +++ b/homeassistant/components/local_todo/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/local_todo", "iot_class": "local_polling", - "requirements": ["ical==10.0.0"] + "requirements": ["ical==10.0.4"] } diff --git a/homeassistant/components/remote_calendar/manifest.json b/homeassistant/components/remote_calendar/manifest.json index 7bdc5362ae7..6da9ce4bb07 100644 --- a/homeassistant/components/remote_calendar/manifest.json +++ b/homeassistant/components/remote_calendar/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["ical"], "quality_scale": "silver", - "requirements": ["ical==10.0.0"] + "requirements": ["ical==10.0.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index fbc8c403f9c..b98f3a65b8b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1203,7 +1203,7 @@ ibmiotf==0.3.4 # homeassistant.components.local_calendar # homeassistant.components.local_todo # homeassistant.components.remote_calendar -ical==10.0.0 +ical==10.0.4 # homeassistant.components.caldav icalendar==6.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5659daa20ee..c0029e20841 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1040,7 +1040,7 @@ ibeacon-ble==1.2.0 # homeassistant.components.local_calendar # homeassistant.components.local_todo # homeassistant.components.remote_calendar -ical==10.0.0 +ical==10.0.4 # homeassistant.components.caldav icalendar==6.1.0 From 912c4804cbc30208d3fa3251690683d6fc98d73a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 17 Jun 2025 14:24:31 +0200 Subject: [PATCH 238/266] Fix incorrect use of zip in service.async_get_all_descriptions (#147013) * Fix incorrect use of zip in service.async_get_all_descriptions * Fix lint errors in test --- homeassistant/helpers/service.py | 10 +++--- tests/helpers/test_service.py | 54 ++++++++++++++++++++++++++------ 2 files changed, 50 insertions(+), 14 deletions(-) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index f157e82bc53..6e1988fe4cd 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -682,9 +682,12 @@ def _load_services_file(hass: HomeAssistant, integration: Integration) -> JSON_T def _load_services_files( hass: HomeAssistant, integrations: Iterable[Integration] -) -> list[JSON_TYPE]: +) -> dict[str, JSON_TYPE]: """Load service files for multiple integrations.""" - return [_load_services_file(hass, integration) for integration in integrations] + return { + integration.domain: _load_services_file(hass, integration) + for integration in integrations + } @callback @@ -744,10 +747,9 @@ async def async_get_all_descriptions( _LOGGER.error("Failed to load integration: %s", domain, exc_info=int_or_exc) if integrations: - contents = await hass.async_add_executor_job( + loaded = await hass.async_add_executor_job( _load_services_files, hass, integrations ) - loaded = dict(zip(domains_with_missing_services, contents, strict=False)) # Load translations for all service domains translations = await translation.async_get_translations( diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 38e7e1ae452..6b464faa110 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -16,6 +16,7 @@ from homeassistant import exceptions from homeassistant.auth.permissions import PolicyPermissions import homeassistant.components # noqa: F401 from homeassistant.components.group import DOMAIN as DOMAIN_GROUP, Group +from homeassistant.components.input_button import DOMAIN as DOMAIN_INPUT_BUTTON from homeassistant.components.logger import DOMAIN as DOMAIN_LOGGER from homeassistant.components.shell_command import DOMAIN as DOMAIN_SHELL_COMMAND from homeassistant.components.system_health import DOMAIN as DOMAIN_SYSTEM_HEALTH @@ -42,7 +43,12 @@ from homeassistant.helpers import ( entity_registry as er, service, ) -from homeassistant.loader import async_get_integration +from homeassistant.helpers.translation import async_get_translations +from homeassistant.loader import ( + Integration, + async_get_integration, + async_get_integrations, +) from homeassistant.setup import async_setup_component from homeassistant.util.yaml.loader import parse_yaml @@ -1092,38 +1098,66 @@ async def test_async_get_all_descriptions_failing_integration( """Test async_get_all_descriptions when async_get_integrations returns an exception.""" group_config = {DOMAIN_GROUP: {}} await async_setup_component(hass, DOMAIN_GROUP, group_config) - descriptions = await service.async_get_all_descriptions(hass) - - assert len(descriptions) == 1 - - assert "description" in descriptions["group"]["reload"] - assert "fields" in descriptions["group"]["reload"] logger_config = {DOMAIN_LOGGER: {}} await async_setup_component(hass, DOMAIN_LOGGER, logger_config) + + input_button_config = {DOMAIN_INPUT_BUTTON: {}} + await async_setup_component(hass, DOMAIN_INPUT_BUTTON, input_button_config) + + async def wrap_get_integrations( + hass: HomeAssistant, domains: Iterable[str] + ) -> dict[str, Integration | Exception]: + integrations = await async_get_integrations(hass, domains) + integrations[DOMAIN_LOGGER] = ImportError("Failed to load services.yaml") + return integrations + + async def wrap_get_translations( + hass: HomeAssistant, + language: str, + category: str, + integrations: Iterable[str] | None = None, + config_flow: bool | None = None, + ) -> dict[str, str]: + translations = await async_get_translations( + hass, language, category, integrations, config_flow + ) + return { + key: value + for key, value in translations.items() + if not key.startswith("component.logger.services.") + } + with ( patch( "homeassistant.helpers.service.async_get_integrations", - return_value={"logger": ImportError}, + wraps=wrap_get_integrations, ), patch( "homeassistant.helpers.service.translation.async_get_translations", - return_value={}, + wrap_get_translations, ), ): descriptions = await service.async_get_all_descriptions(hass) - assert len(descriptions) == 2 + assert len(descriptions) == 3 assert "Failed to load integration: logger" in caplog.text # Services are empty defaults if the load fails but should # not raise + assert descriptions[DOMAIN_GROUP]["remove"]["description"] + assert descriptions[DOMAIN_GROUP]["remove"]["fields"] + assert descriptions[DOMAIN_LOGGER]["set_level"] == { "description": "", "fields": {}, "name": "", } + assert descriptions[DOMAIN_INPUT_BUTTON]["press"]["description"] + assert descriptions[DOMAIN_INPUT_BUTTON]["press"]["fields"] == {} + assert "target" in descriptions[DOMAIN_INPUT_BUTTON]["press"] + hass.services.async_register(DOMAIN_LOGGER, "new_service", lambda x: None, None) service.async_set_service_schema( hass, DOMAIN_LOGGER, "new_service", {"description": "new service"} From 5ea6cb384625a38f9531346f8e05b5ec66c21b01 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 17 Jun 2025 14:39:22 +0200 Subject: [PATCH 239/266] Disable Z-Wave indidator CC entities by default (#147018) * Update discovery tests * Disable Z-Wave indidator CC entities by default --- .../components/zwave_js/discovery.py | 4 + tests/components/zwave_js/test_discovery.py | 115 ++++++++++-------- 2 files changed, 68 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index b46735e4040..924778a9e5b 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -896,6 +896,7 @@ DISCOVERY_SCHEMAS = [ writeable=False, ), entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, ), # generic text sensors ZWaveDiscoverySchema( @@ -932,6 +933,7 @@ DISCOVERY_SCHEMAS = [ ), data_template=NumericSensorDataTemplate(), entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, ), # Meter sensors for Meter CC ZWaveDiscoverySchema( @@ -957,6 +959,7 @@ DISCOVERY_SCHEMAS = [ writeable=True, ), entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, ), # button for Indicator CC ZWaveDiscoverySchema( @@ -980,6 +983,7 @@ DISCOVERY_SCHEMAS = [ writeable=True, ), entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, ), # binary switch # barrier operator signaling states diff --git a/tests/components/zwave_js/test_discovery.py b/tests/components/zwave_js/test_discovery.py index 7ef5f0e480f..02296262d1f 100644 --- a/tests/components/zwave_js/test_discovery.py +++ b/tests/components/zwave_js/test_discovery.py @@ -1,10 +1,12 @@ """Test entity discovery for device-specific schemas for the Z-Wave JS integration.""" +from datetime import timedelta +from unittest.mock import MagicMock + import pytest from zwave_js_server.event import Event from zwave_js_server.model.node import Node -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.components.light import ATTR_SUPPORTED_COLOR_MODES, ColorMode from homeassistant.components.number import ( @@ -12,7 +14,6 @@ from homeassistant.components.number import ( DOMAIN as NUMBER_DOMAIN, SERVICE_SET_VALUE, ) -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import ( DOMAIN as SWITCH_DOMAIN, SERVICE_TURN_OFF, @@ -26,12 +27,13 @@ from homeassistant.components.zwave_js.discovery import ( from homeassistant.components.zwave_js.discovery_data_template import ( DynamicCurrentTempClimateDataTemplate, ) -from homeassistant.components.zwave_js.helpers import get_device_id +from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_UNKNOWN, EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.util import dt as dt_util -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed async def test_aeon_smart_switch_6_state( @@ -222,17 +224,24 @@ async def test_merten_507801_disabled_enitites( async def test_zooz_zen72( hass: HomeAssistant, entity_registry: er.EntityRegistry, - client, - switch_zooz_zen72, - integration, + client: MagicMock, + switch_zooz_zen72: Node, + integration: MockConfigEntry, ) -> None: """Test that Zooz ZEN72 Indicators are discovered as number entities.""" - assert len(hass.states.async_entity_ids(NUMBER_DOMAIN)) == 1 - assert len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == 2 # includes ping entity_id = "number.z_wave_plus_700_series_dimmer_switch_indicator_value" - entry = entity_registry.async_get(entity_id) - assert entry - assert entry.entity_category == EntityCategory.CONFIG + entity_entry = entity_registry.async_get(entity_id) + assert entity_entry + assert entity_entry.entity_category == EntityCategory.CONFIG + assert entity_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION + assert hass.states.get(entity_id) is None # disabled by default + entity_registry.async_update_entity(entity_id, disabled_by=None) + async_fire_time_changed( + hass, + dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), + ) + await hass.async_block_till_done() + client.async_send_command.reset_mock() state = hass.states.get(entity_id) assert state assert state.state == STATE_UNKNOWN @@ -246,7 +255,7 @@ async def test_zooz_zen72( }, blocking=True, ) - assert len(client.async_send_command.call_args_list) == 1 + assert client.async_send_command.call_count == 1 args = client.async_send_command.call_args[0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == switch_zooz_zen72.node_id @@ -260,16 +269,18 @@ async def test_zooz_zen72( client.async_send_command.reset_mock() entity_id = "button.z_wave_plus_700_series_dimmer_switch_identify" - entry = entity_registry.async_get(entity_id) - assert entry - assert entry.entity_category == EntityCategory.CONFIG + entity_entry = entity_registry.async_get(entity_id) + assert entity_entry + assert entity_entry.entity_category == EntityCategory.CONFIG + assert entity_entry.disabled_by is None + assert hass.states.get(entity_id) is not None await hass.services.async_call( BUTTON_DOMAIN, SERVICE_PRESS, {ATTR_ENTITY_ID: entity_id}, blocking=True, ) - assert len(client.async_send_command.call_args_list) == 1 + assert client.async_send_command.call_count == 1 args = client.async_send_command.call_args[0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == switch_zooz_zen72.node_id @@ -285,53 +296,55 @@ async def test_indicator_test( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - client, - indicator_test, - integration, + client: MagicMock, + indicator_test: Node, + integration: MockConfigEntry, ) -> None: """Test that Indicators are discovered properly. This test covers indicators that we don't already have device fixtures for. """ - device = device_registry.async_get_device( - identifiers={get_device_id(client.driver, indicator_test)} + binary_sensor_entity_id = "binary_sensor.this_is_a_fake_device_binary_sensor" + sensor_entity_id = "sensor.this_is_a_fake_device_sensor" + switch_entity_id = "switch.this_is_a_fake_device_switch" + + for entity_id in ( + binary_sensor_entity_id, + sensor_entity_id, + ): + entity_entry = entity_registry.async_get(entity_id) + assert entity_entry + assert entity_entry.entity_category == EntityCategory.DIAGNOSTIC + assert entity_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION + assert hass.states.get(entity_id) is None # disabled by default + entity_registry.async_update_entity(entity_id, disabled_by=None) + + entity_id = switch_entity_id + entity_entry = entity_registry.async_get(entity_id) + assert entity_entry + assert entity_entry.entity_category == EntityCategory.CONFIG + assert entity_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION + assert hass.states.get(entity_id) is None # disabled by default + entity_registry.async_update_entity(entity_id, disabled_by=None) + + async_fire_time_changed( + hass, + dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), ) - assert device - entities = er.async_entries_for_device(entity_registry, device.id) + await hass.async_block_till_done() + client.async_send_command.reset_mock() - def len_domain(domain): - return len([entity for entity in entities if entity.domain == domain]) - - assert len_domain(NUMBER_DOMAIN) == 0 - assert len_domain(BUTTON_DOMAIN) == 1 # only ping - assert len_domain(BINARY_SENSOR_DOMAIN) == 1 - assert len_domain(SENSOR_DOMAIN) == 3 # include node status + last seen - assert len_domain(SWITCH_DOMAIN) == 1 - - entity_id = "binary_sensor.this_is_a_fake_device_binary_sensor" - entry = entity_registry.async_get(entity_id) - assert entry - assert entry.entity_category == EntityCategory.DIAGNOSTIC + entity_id = binary_sensor_entity_id state = hass.states.get(entity_id) assert state assert state.state == STATE_OFF - client.async_send_command.reset_mock() - - entity_id = "sensor.this_is_a_fake_device_sensor" - entry = entity_registry.async_get(entity_id) - assert entry - assert entry.entity_category == EntityCategory.DIAGNOSTIC + entity_id = sensor_entity_id state = hass.states.get(entity_id) assert state assert state.state == "0.0" - client.async_send_command.reset_mock() - - entity_id = "switch.this_is_a_fake_device_switch" - entry = entity_registry.async_get(entity_id) - assert entry - assert entry.entity_category == EntityCategory.CONFIG + entity_id = switch_entity_id state = hass.states.get(entity_id) assert state assert state.state == STATE_OFF @@ -342,7 +355,7 @@ async def test_indicator_test( {ATTR_ENTITY_ID: entity_id}, blocking=True, ) - assert len(client.async_send_command.call_args_list) == 1 + assert client.async_send_command.call_count == 1 args = client.async_send_command.call_args[0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == indicator_test.node_id @@ -362,7 +375,7 @@ async def test_indicator_test( {ATTR_ENTITY_ID: entity_id}, blocking=True, ) - assert len(client.async_send_command.call_args_list) == 1 + assert client.async_send_command.call_count == 1 args = client.async_send_command.call_args[0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == indicator_test.node_id From 766ddfaacc1fbc245fc2cf8f84531fa559c6e024 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 20 Jun 2025 15:19:39 +0200 Subject: [PATCH 240/266] Fix Shelly entity names for gen1 sleeping devices (#147019) --- homeassistant/components/shelly/entity.py | 1 - tests/components/shelly/test_sensor.py | 44 +++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 2c1678d56d9..5a420a4543b 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -653,7 +653,6 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity): ) elif entry is not None: self._attr_unique_id = entry.unique_id - self._attr_name = cast(str, entry.original_name) @callback def _update_callback(self) -> None: diff --git a/tests/components/shelly/test_sensor.py b/tests/components/shelly/test_sensor.py index e95d4cfaeb2..8f021c2d58a 100644 --- a/tests/components/shelly/test_sensor.py +++ b/tests/components/shelly/test_sensor.py @@ -23,6 +23,7 @@ from homeassistant.components.shelly.const import DOMAIN from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, STATE_UNAVAILABLE, @@ -40,6 +41,7 @@ from homeassistant.helpers.entity_registry import EntityRegistry from homeassistant.setup import async_setup_component from . import ( + MOCK_MAC, init_integration, mock_polling_rpc_update, mock_rest_update, @@ -1585,3 +1587,45 @@ async def test_rpc_switch_no_returned_energy_sensor( await init_integration(hass, 3) assert hass.states.get("sensor.test_name_test_switch_0_returned_energy") is None + + +async def test_block_friendly_name_sleeping_sensor( + hass: HomeAssistant, + mock_block_device: Mock, + device_registry: DeviceRegistry, + entity_registry: EntityRegistry, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test friendly name for restored sleeping sensor.""" + entry = await init_integration(hass, 1, sleep_period=1000, skip_setup=True) + device = register_device(device_registry, entry) + + entity = entity_registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + f"{MOCK_MAC}-sensor_0-temp", + suggested_object_id="test_name_temperature", + original_name="Test name temperature", + disabled_by=None, + config_entry=entry, + device_id=device.id, + ) + + # Old name, the word "temperature" starts with a lower case letter + assert entity.original_name == "Test name temperature" + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert (state := hass.states.get(entity.entity_id)) + + # New name, the word "temperature" starts with a capital letter + assert state.attributes[ATTR_FRIENDLY_NAME] == "Test name Temperature" + + # Make device online + monkeypatch.setattr(mock_block_device, "initialized", True) + mock_block_device.mock_online() + await hass.async_block_till_done(wait_background_tasks=True) + + assert (state := hass.states.get(entity.entity_id)) + assert state.attributes[ATTR_FRIENDLY_NAME] == "Test name Temperature" From 96d6cacae411458ea08c87b90e8fdf064751973c Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 18 Jun 2025 07:29:04 +0200 Subject: [PATCH 241/266] Disable Z-Wave idle notification button (#147026) * Update test * Disable Z-Wave idle notification button * Update tests --- .../components/zwave_js/discovery.py | 1 + .../zwave_js/snapshots/test_diagnostics.ambr | 8 ++-- tests/components/zwave_js/test_button.py | 41 ++++++++++++++++--- tests/components/zwave_js/test_init.py | 12 +----- 4 files changed, 42 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 924778a9e5b..4e9a3321beb 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -1188,6 +1188,7 @@ DISCOVERY_SCHEMAS = [ any_available_states={(0, "idle")}, ), allow_multi=True, + entity_registry_enabled_default=False, ), # event # stateful = False diff --git a/tests/components/zwave_js/snapshots/test_diagnostics.ambr b/tests/components/zwave_js/snapshots/test_diagnostics.ambr index dc0dbba59b5..40ed3bbf836 100644 --- a/tests/components/zwave_js/snapshots/test_diagnostics.ambr +++ b/tests/components/zwave_js/snapshots/test_diagnostics.ambr @@ -97,8 +97,8 @@ 'value_id': '52-113-0-Home Security-Cover status', }), dict({ - 'disabled': False, - 'disabled_by': None, + 'disabled': True, + 'disabled_by': 'integration', 'domain': 'button', 'entity_category': 'config', 'entity_id': 'button.multisensor_6_idle_home_security_cover_status', @@ -120,8 +120,8 @@ 'value_id': '52-113-0-Home Security-Cover status', }), dict({ - 'disabled': False, - 'disabled_by': None, + 'disabled': True, + 'disabled_by': 'integration', 'domain': 'button', 'entity_category': 'config', 'entity_id': 'button.multisensor_6_idle_home_security_motion_sensor_status', diff --git a/tests/components/zwave_js/test_button.py b/tests/components/zwave_js/test_button.py index 0282a268b54..422888cab23 100644 --- a/tests/components/zwave_js/test_button.py +++ b/tests/components/zwave_js/test_button.py @@ -1,13 +1,21 @@ """Test the Z-Wave JS button entities.""" +from datetime import timedelta +from unittest.mock import MagicMock + import pytest +from zwave_js_server.model.node import Node from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS 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, Platform +from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY +from homeassistant.const import ATTR_ENTITY_ID, EntityCategory, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from homeassistant.util import dt as dt_util + +from tests.common import MockConfigEntry, async_fire_time_changed @pytest.fixture @@ -71,11 +79,32 @@ async def test_ping_entity( async def test_notification_idle_button( - hass: HomeAssistant, client, multisensor_6, integration + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + client: MagicMock, + multisensor_6: Node, + integration: MockConfigEntry, ) -> None: """Test Notification idle button.""" node = multisensor_6 - state = hass.states.get("button.multisensor_6_idle_home_security_cover_status") + entity_id = "button.multisensor_6_idle_home_security_cover_status" + entity_entry = entity_registry.async_get(entity_id) + assert entity_entry + assert entity_entry.entity_category is EntityCategory.CONFIG + assert entity_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION + assert hass.states.get(entity_id) is None # disabled by default + + entity_registry.async_update_entity( + entity_id, + disabled_by=None, + ) + async_fire_time_changed( + hass, + dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), + ) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) assert state assert state.state == "unknown" assert ( @@ -88,13 +117,13 @@ async def test_notification_idle_button( BUTTON_DOMAIN, SERVICE_PRESS, { - ATTR_ENTITY_ID: "button.multisensor_6_idle_home_security_cover_status", + ATTR_ENTITY_ID: entity_id, }, blocking=True, ) - assert len(client.async_send_command_no_wait.call_args_list) == 1 - args = client.async_send_command_no_wait.call_args_list[0][0][0] + assert client.async_send_command_no_wait.call_count == 1 + args = client.async_send_command_no_wait.call_args[0][0] assert args["command"] == "node.manually_idle_notification_value" assert args["nodeId"] == node.node_id assert args["valueId"] == { diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index ef74373ad9e..fa82b051e59 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -1812,7 +1812,8 @@ async def test_disabled_node_status_entity_on_node_replaced( assert state.state == STATE_UNAVAILABLE -async def test_disabled_entity_on_value_removed( +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_remove_entity_on_value_removed( hass: HomeAssistant, zp3111: Node, client: MagicMock, @@ -1823,15 +1824,6 @@ async def test_disabled_entity_on_value_removed( "button.4_in_1_sensor_idle_home_security_cover_status" ) - # must reload the integration when enabling an entity - await hass.config_entries.async_unload(integration.entry_id) - await hass.async_block_till_done() - assert integration.state is ConfigEntryState.NOT_LOADED - integration.add_to_hass(hass) - await hass.config_entries.async_setup(integration.entry_id) - await hass.async_block_till_done() - assert integration.state is ConfigEntryState.LOADED - state = hass.states.get(idle_cover_status_button_entity) assert state assert state.state != STATE_UNAVAILABLE From a07531d0e78a47c73118fa15fddaff0b959ad9a9 Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Tue, 17 Jun 2025 19:57:52 +0200 Subject: [PATCH 242/266] Fix log in onedrive (#147029) --- homeassistant/components/onedrive/coordinator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/onedrive/coordinator.py b/homeassistant/components/onedrive/coordinator.py index ff05b19f84d..07a8dbd203b 100644 --- a/homeassistant/components/onedrive/coordinator.py +++ b/homeassistant/components/onedrive/coordinator.py @@ -66,7 +66,7 @@ class OneDriveUpdateCoordinator(DataUpdateCoordinator[Drive]): translation_domain=DOMAIN, translation_key="authentication_failed" ) from err except OneDriveException as err: - _LOGGER.debug("Failed to fetch drive data: %s") + _LOGGER.debug("Failed to fetch drive data: %s", err, exc_info=True) raise UpdateFailed( translation_domain=DOMAIN, translation_key="update_failed" ) from err From a15d722f0eda23fea5b87387b92de616014b4896 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 18 Jun 2025 09:11:01 +0200 Subject: [PATCH 243/266] Bump holidays lib to 0.75 (#147043) --- homeassistant/components/holiday/manifest.json | 2 +- homeassistant/components/workday/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/holiday/manifest.json b/homeassistant/components/holiday/manifest.json index 5a5f1daf967..c76d6638730 100644 --- a/homeassistant/components/holiday/manifest.json +++ b/homeassistant/components/holiday/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/holiday", "iot_class": "local_polling", - "requirements": ["holidays==0.74", "babel==2.15.0"] + "requirements": ["holidays==0.75", "babel==2.15.0"] } diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index 9091dd131dd..f9fae38f1f5 100644 --- a/homeassistant/components/workday/manifest.json +++ b/homeassistant/components/workday/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_polling", "loggers": ["holidays"], "quality_scale": "internal", - "requirements": ["holidays==0.74"] + "requirements": ["holidays==0.75"] } diff --git a/requirements_all.txt b/requirements_all.txt index b98f3a65b8b..4f16704fa74 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1161,7 +1161,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.74 +holidays==0.75 # homeassistant.components.frontend home-assistant-frontend==20250531.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c0029e20841..6f50bd8f64c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1007,7 +1007,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.74 +holidays==0.75 # homeassistant.components.frontend home-assistant-frontend==20250531.3 From f75ba9172c54792ea3ee9cd46b295f6a4bce6101 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Diego=20Rodr=C3=ADguez=20Royo?= Date: Tue, 17 Jun 2025 20:48:09 +0200 Subject: [PATCH 244/266] Bump aiohomeconnect to 0.18.0 (#147044) --- homeassistant/components/home_connect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/home_connect/manifest.json b/homeassistant/components/home_connect/manifest.json index d4b37552fb7..34a3b756119 100644 --- a/homeassistant/components/home_connect/manifest.json +++ b/homeassistant/components/home_connect/manifest.json @@ -21,6 +21,6 @@ "documentation": "https://www.home-assistant.io/integrations/home_connect", "iot_class": "cloud_push", "loggers": ["aiohomeconnect"], - "requirements": ["aiohomeconnect==0.17.1"], + "requirements": ["aiohomeconnect==0.18.0"], "zeroconf": ["_homeconnect._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 4f16704fa74..361e6932dff 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -265,7 +265,7 @@ aioharmony==0.5.2 aiohasupervisor==0.3.1 # homeassistant.components.home_connect -aiohomeconnect==0.17.1 +aiohomeconnect==0.18.0 # homeassistant.components.homekit_controller aiohomekit==3.2.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6f50bd8f64c..cfe2f88c026 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -250,7 +250,7 @@ aioharmony==0.5.2 aiohasupervisor==0.3.1 # homeassistant.components.home_connect -aiohomeconnect==0.17.1 +aiohomeconnect==0.18.0 # homeassistant.components.homekit_controller aiohomekit==3.2.14 From 57eceeea38f16efd356eda8bd4909a151836c12c Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 17 Jun 2025 15:06:53 -0400 Subject: [PATCH 245/266] Bump ZHA to 0.0.60 (#147045) --- 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 4a5ec7be1dc..4908298847b 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.59"], + "requirements": ["zha==0.0.60"], "usb": [ { "vid": "10C4", diff --git a/requirements_all.txt b/requirements_all.txt index 361e6932dff..20602c5be75 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3180,7 +3180,7 @@ zeroconf==0.147.0 zeversolar==0.3.2 # homeassistant.components.zha -zha==0.0.59 +zha==0.0.60 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cfe2f88c026..80810a178ce 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2621,7 +2621,7 @@ zeroconf==0.147.0 zeversolar==0.3.2 # homeassistant.components.zha -zha==0.0.59 +zha==0.0.60 # homeassistant.components.zwave_js zwave-js-server-python==0.63.0 From c395c77cd3f18b892b288e1f2306c901fb69b3c0 Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Tue, 17 Jun 2025 20:59:41 +0200 Subject: [PATCH 246/266] Bump pylamarzocco to 2.0.9 (#147046) --- 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 46a29427264..7fdafc4dda1 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.8"] + "requirements": ["pylamarzocco==2.0.9"] } diff --git a/requirements_all.txt b/requirements_all.txt index 20602c5be75..b1a4c497e55 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2096,7 +2096,7 @@ pykwb==0.0.8 pylacrosse==0.4 # homeassistant.components.lamarzocco -pylamarzocco==2.0.8 +pylamarzocco==2.0.9 # homeassistant.components.lastfm pylast==5.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 80810a178ce..766f9365188 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1738,7 +1738,7 @@ pykrakenapi==0.1.8 pykulersky==0.5.8 # homeassistant.components.lamarzocco -pylamarzocco==2.0.8 +pylamarzocco==2.0.9 # homeassistant.components.lastfm pylast==5.1.0 From 2c357265b0343dcf859b0ea30a8023c8a9567506 Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Tue, 17 Jun 2025 21:25:27 +0200 Subject: [PATCH 247/266] Handle missing widget in lamarzocco (#147047) --- homeassistant/components/lamarzocco/number.py | 6 +++++- homeassistant/components/lamarzocco/sensor.py | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/lamarzocco/number.py b/homeassistant/components/lamarzocco/number.py index 980a08c09ae..f8cb8b1d6fe 100644 --- a/homeassistant/components/lamarzocco/number.py +++ b/homeassistant/components/lamarzocco/number.py @@ -58,6 +58,10 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = ( CoffeeBoiler, machine.dashboard.config[WidgetType.CM_COFFEE_BOILER] ).target_temperature ), + available_fn=( + lambda coordinator: WidgetType.CM_COFFEE_BOILER + in coordinator.device.dashboard.config + ), ), LaMarzoccoNumberEntityDescription( key="smart_standby_time", @@ -221,7 +225,7 @@ class LaMarzoccoNumberEntity(LaMarzoccoEntity, NumberEntity): entity_description: LaMarzoccoNumberEntityDescription @property - def native_value(self) -> float: + def native_value(self) -> float | int: """Return the current value.""" return self.entity_description.native_value_fn(self.coordinator.device) diff --git a/homeassistant/components/lamarzocco/sensor.py b/homeassistant/components/lamarzocco/sensor.py index 29f1c6209ec..c76f51c3488 100644 --- a/homeassistant/components/lamarzocco/sensor.py +++ b/homeassistant/components/lamarzocco/sensor.py @@ -57,6 +57,10 @@ ENTITIES: tuple[LaMarzoccoSensorEntityDescription, ...] = ( ).ready_start_time ), entity_category=EntityCategory.DIAGNOSTIC, + available_fn=( + lambda coordinator: WidgetType.CM_COFFEE_BOILER + in coordinator.device.dashboard.config + ), ), LaMarzoccoSensorEntityDescription( key="steam_boiler_ready_time", From 1a3384e8b47662ddea6061ec713733087ac3013b Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sun, 15 Jun 2025 18:30:19 +0300 Subject: [PATCH 248/266] Bump aioamazondevices to 3.1.4 (#146883) --- 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 0ea4c32a75e..7a7713f861b 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.1.3"] + "requirements": ["aioamazondevices==3.1.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index b1a4c497e55..275595cb0d3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -182,7 +182,7 @@ aioairzone-cloud==0.6.12 aioairzone==1.0.0 # homeassistant.components.alexa_devices -aioamazondevices==3.1.3 +aioamazondevices==3.1.4 # homeassistant.components.ambient_network # homeassistant.components.ambient_station diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 766f9365188..2a1287da822 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -170,7 +170,7 @@ aioairzone-cloud==0.6.12 aioairzone==1.0.0 # homeassistant.components.alexa_devices -aioamazondevices==3.1.3 +aioamazondevices==3.1.4 # homeassistant.components.ambient_network # homeassistant.components.ambient_station From 0b383b7493ea38425a7f6ec8a8c960da330ca440 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Wed, 18 Jun 2025 11:20:51 +0300 Subject: [PATCH 249/266] Bump aioamazondevices to 3.1.12 (#147055) * Bump aioamazondevices to 3.1.10 * bump to 3.1.12 --- 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 7a7713f861b..aeecb5bc96c 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.1.4"] + "requirements": ["aioamazondevices==3.1.12"] } diff --git a/requirements_all.txt b/requirements_all.txt index 275595cb0d3..d567bf86353 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -182,7 +182,7 @@ aioairzone-cloud==0.6.12 aioairzone==1.0.0 # homeassistant.components.alexa_devices -aioamazondevices==3.1.4 +aioamazondevices==3.1.12 # homeassistant.components.ambient_network # homeassistant.components.ambient_station diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2a1287da822..074194d5da3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -170,7 +170,7 @@ aioairzone-cloud==0.6.12 aioairzone==1.0.0 # homeassistant.components.alexa_devices -aioamazondevices==3.1.4 +aioamazondevices==3.1.12 # homeassistant.components.ambient_network # homeassistant.components.ambient_station From c66d411826f340d305fcb553560fed726adea14e Mon Sep 17 00:00:00 2001 From: Raphael Hehl <7577984+RaHehl@users.noreply.github.com> Date: Thu, 19 Jun 2025 11:20:29 +0200 Subject: [PATCH 250/266] Bump uiprotect to version 7.14.0 (#147102) --- 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 3c32935a995..f99d910adf9 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.13.0", "unifi-discovery==1.2.0"], + "requirements": ["uiprotect==7.14.0", "unifi-discovery==1.2.0"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index d567bf86353..6d09a5f9037 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2987,7 +2987,7 @@ typedmonarchmoney==0.4.4 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==7.13.0 +uiprotect==7.14.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 074194d5da3..6c65be62ef2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2458,7 +2458,7 @@ typedmonarchmoney==0.4.4 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==7.13.0 +uiprotect==7.14.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From 39b64b0af3de3c64a32f28ec70ea30ec3f405810 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 19 Jun 2025 17:56:47 +0200 Subject: [PATCH 251/266] Improve advanced Z-Wave battery discovery (#147127) --- .../components/zwave_js/binary_sensor.py | 34 +- homeassistant/components/zwave_js/const.py | 5 +- .../components/zwave_js/discovery.py | 31 +- .../zwave_js/discovery_data_template.py | 32 +- homeassistant/components/zwave_js/sensor.py | 47 +- tests/components/zwave_js/common.py | 1 - tests/components/zwave_js/conftest.py | 14 + .../zwave_js/fixtures/ring_keypad_state.json | 7543 +++++++++++++++++ .../components/zwave_js/test_binary_sensor.py | 52 +- tests/components/zwave_js/test_sensor.py | 93 +- 10 files changed, 7825 insertions(+), 27 deletions(-) create mode 100644 tests/components/zwave_js/fixtures/ring_keypad_state.json diff --git a/homeassistant/components/zwave_js/binary_sensor.py b/homeassistant/components/zwave_js/binary_sensor.py index 1439aa0ca0f..d70690ace31 100644 --- a/homeassistant/components/zwave_js/binary_sensor.py +++ b/homeassistant/components/zwave_js/binary_sensor.py @@ -318,12 +318,37 @@ PROPERTY_SENSOR_MAPPINGS: dict[str, PropertyZWaveJSEntityDescription] = { # Mappings for boolean sensors -BOOLEAN_SENSOR_MAPPINGS: dict[int, BinarySensorEntityDescription] = { - CommandClass.BATTERY: BinarySensorEntityDescription( - key=str(CommandClass.BATTERY), +BOOLEAN_SENSOR_MAPPINGS: dict[tuple[int, int | str], BinarySensorEntityDescription] = { + (CommandClass.BATTERY, "backup"): BinarySensorEntityDescription( + key="battery_backup", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + (CommandClass.BATTERY, "disconnected"): BinarySensorEntityDescription( + key="battery_disconnected", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + (CommandClass.BATTERY, "isLow"): BinarySensorEntityDescription( + key="battery_is_low", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, ), + (CommandClass.BATTERY, "lowFluid"): BinarySensorEntityDescription( + key="battery_low_fluid", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + (CommandClass.BATTERY, "overheating"): BinarySensorEntityDescription( + key="battery_overheating", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + (CommandClass.BATTERY, "rechargeable"): BinarySensorEntityDescription( + key="battery_rechargeable", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), } @@ -432,8 +457,9 @@ class ZWaveBooleanBinarySensor(ZWaveBaseEntity, BinarySensorEntity): # Entity class attributes self._attr_name = self.generate_name(include_value_name=True) + primary_value = self.info.primary_value if description := BOOLEAN_SENSOR_MAPPINGS.get( - self.info.primary_value.command_class + (primary_value.command_class, primary_value.property_) ): self.entity_description = description diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index 3d626710d52..a99e9fd0113 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -139,7 +139,10 @@ ATTR_TWIST_ASSIST = "twist_assist" ADDON_SLUG = "core_zwave_js" # Sensor entity description constants -ENTITY_DESC_KEY_BATTERY = "battery" +ENTITY_DESC_KEY_BATTERY_LEVEL = "battery_level" +ENTITY_DESC_KEY_BATTERY_LIST_STATE = "battery_list_state" +ENTITY_DESC_KEY_BATTERY_MAXIMUM_CAPACITY = "battery_maximum_capacity" +ENTITY_DESC_KEY_BATTERY_TEMPERATURE = "battery_temperature" ENTITY_DESC_KEY_CURRENT = "current" ENTITY_DESC_KEY_VOLTAGE = "voltage" ENTITY_DESC_KEY_ENERGY_MEASUREMENT = "energy_measurement" diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 4e9a3321beb..92233dd2e77 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -913,7 +913,6 @@ DISCOVERY_SCHEMAS = [ hint="numeric_sensor", primary_value=ZWaveValueDiscoverySchema( command_class={ - CommandClass.BATTERY, CommandClass.ENERGY_PRODUCTION, CommandClass.SENSOR_ALARM, CommandClass.SENSOR_MULTILEVEL, @@ -922,6 +921,36 @@ DISCOVERY_SCHEMAS = [ ), data_template=NumericSensorDataTemplate(), ), + ZWaveDiscoverySchema( + platform=Platform.SENSOR, + hint="numeric_sensor", + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.BATTERY}, + type={ValueType.NUMBER}, + property={"level", "maximumCapacity"}, + ), + data_template=NumericSensorDataTemplate(), + ), + ZWaveDiscoverySchema( + platform=Platform.SENSOR, + hint="numeric_sensor", + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.BATTERY}, + type={ValueType.NUMBER}, + property={"temperature"}, + ), + data_template=NumericSensorDataTemplate(), + ), + ZWaveDiscoverySchema( + platform=Platform.SENSOR, + hint="list", + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.BATTERY}, + type={ValueType.NUMBER}, + property={"chargingStatus", "rechargeOrReplace"}, + ), + data_template=NumericSensorDataTemplate(), + ), ZWaveDiscoverySchema( platform=Platform.SENSOR, hint="numeric_sensor", diff --git a/homeassistant/components/zwave_js/discovery_data_template.py b/homeassistant/components/zwave_js/discovery_data_template.py index e619c6afc7c..731a786d226 100644 --- a/homeassistant/components/zwave_js/discovery_data_template.py +++ b/homeassistant/components/zwave_js/discovery_data_template.py @@ -133,7 +133,10 @@ from homeassistant.const import ( ) from .const import ( - ENTITY_DESC_KEY_BATTERY, + ENTITY_DESC_KEY_BATTERY_LEVEL, + ENTITY_DESC_KEY_BATTERY_LIST_STATE, + ENTITY_DESC_KEY_BATTERY_MAXIMUM_CAPACITY, + ENTITY_DESC_KEY_BATTERY_TEMPERATURE, ENTITY_DESC_KEY_CO, ENTITY_DESC_KEY_CO2, ENTITY_DESC_KEY_CURRENT, @@ -380,8 +383,31 @@ class NumericSensorDataTemplate(BaseDiscoverySchemaDataTemplate): def resolve_data(self, value: ZwaveValue) -> NumericSensorDataTemplateData: """Resolve helper class data for a discovered value.""" - if value.command_class == CommandClass.BATTERY: - return NumericSensorDataTemplateData(ENTITY_DESC_KEY_BATTERY, PERCENTAGE) + if value.command_class == CommandClass.BATTERY and value.property_ == "level": + return NumericSensorDataTemplateData( + ENTITY_DESC_KEY_BATTERY_LEVEL, PERCENTAGE + ) + if value.command_class == CommandClass.BATTERY and value.property_ in ( + "chargingStatus", + "rechargeOrReplace", + ): + return NumericSensorDataTemplateData( + ENTITY_DESC_KEY_BATTERY_LIST_STATE, None + ) + if ( + value.command_class == CommandClass.BATTERY + and value.property_ == "maximumCapacity" + ): + return NumericSensorDataTemplateData( + ENTITY_DESC_KEY_BATTERY_MAXIMUM_CAPACITY, PERCENTAGE + ) + if ( + value.command_class == CommandClass.BATTERY + and value.property_ == "temperature" + ): + return NumericSensorDataTemplateData( + ENTITY_DESC_KEY_BATTERY_TEMPERATURE, UnitOfTemperature.CELSIUS + ) if value.command_class == CommandClass.METER: try: diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index 4db14d003b1..05fa785760b 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -58,7 +58,10 @@ from .const import ( ATTR_VALUE, DATA_CLIENT, DOMAIN, - ENTITY_DESC_KEY_BATTERY, + ENTITY_DESC_KEY_BATTERY_LEVEL, + ENTITY_DESC_KEY_BATTERY_LIST_STATE, + ENTITY_DESC_KEY_BATTERY_MAXIMUM_CAPACITY, + ENTITY_DESC_KEY_BATTERY_TEMPERATURE, ENTITY_DESC_KEY_CO, ENTITY_DESC_KEY_CO2, ENTITY_DESC_KEY_CURRENT, @@ -95,17 +98,33 @@ from .migrate import async_migrate_statistics_sensors PARALLEL_UPDATES = 0 -# These descriptions should include device class. -ENTITY_DESCRIPTION_KEY_DEVICE_CLASS_MAP: dict[ - tuple[str, str], SensorEntityDescription -] = { - (ENTITY_DESC_KEY_BATTERY, PERCENTAGE): SensorEntityDescription( - key=ENTITY_DESC_KEY_BATTERY, +# These descriptions should have a non None unit of measurement. +ENTITY_DESCRIPTION_KEY_UNIT_MAP: dict[tuple[str, str], SensorEntityDescription] = { + (ENTITY_DESC_KEY_BATTERY_LEVEL, PERCENTAGE): SensorEntityDescription( + key=ENTITY_DESC_KEY_BATTERY_LEVEL, device_class=SensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, ), + (ENTITY_DESC_KEY_BATTERY_MAXIMUM_CAPACITY, PERCENTAGE): SensorEntityDescription( + key=ENTITY_DESC_KEY_BATTERY_MAXIMUM_CAPACITY, + entity_category=EntityCategory.DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=PERCENTAGE, + entity_registry_enabled_default=False, + ), + ( + ENTITY_DESC_KEY_BATTERY_TEMPERATURE, + UnitOfTemperature.CELSIUS, + ): SensorEntityDescription( + key=ENTITY_DESC_KEY_BATTERY_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, + entity_category=EntityCategory.DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + entity_registry_enabled_default=False, + ), (ENTITY_DESC_KEY_CURRENT, UnitOfElectricCurrent.AMPERE): SensorEntityDescription( key=ENTITY_DESC_KEY_CURRENT, device_class=SensorDeviceClass.CURRENT, @@ -285,8 +304,14 @@ ENTITY_DESCRIPTION_KEY_DEVICE_CLASS_MAP: dict[ ), } -# These descriptions are without device class. +# These descriptions are without unit of measurement. ENTITY_DESCRIPTION_KEY_MAP = { + ENTITY_DESC_KEY_BATTERY_LIST_STATE: SensorEntityDescription( + key=ENTITY_DESC_KEY_BATTERY_LIST_STATE, + device_class=SensorDeviceClass.ENUM, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), ENTITY_DESC_KEY_CO: SensorEntityDescription( key=ENTITY_DESC_KEY_CO, state_class=SensorStateClass.MEASUREMENT, @@ -538,7 +563,7 @@ def get_entity_description( """Return the entity description for the given data.""" data_description_key = data.entity_description_key or "" data_unit = data.unit_of_measurement or "" - return ENTITY_DESCRIPTION_KEY_DEVICE_CLASS_MAP.get( + return ENTITY_DESCRIPTION_KEY_UNIT_MAP.get( (data_description_key, data_unit), ENTITY_DESCRIPTION_KEY_MAP.get( data_description_key, @@ -588,6 +613,10 @@ async def async_setup_entry( entities.append( ZWaveListSensor(config_entry, driver, info, entity_description) ) + elif info.platform_hint == "list": + entities.append( + ZWaveListSensor(config_entry, driver, info, entity_description) + ) elif info.platform_hint == "config_parameter": entities.append( ZWaveConfigParameterSensor( diff --git a/tests/components/zwave_js/common.py b/tests/components/zwave_js/common.py index 64bc981de11..578eeab5ec7 100644 --- a/tests/components/zwave_js/common.py +++ b/tests/components/zwave_js/common.py @@ -21,7 +21,6 @@ ENERGY_SENSOR = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed_2" VOLTAGE_SENSOR = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed_3" CURRENT_SENSOR = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed_4" SWITCH_ENTITY = "switch.smart_plug_with_two_usb_ports" -LOW_BATTERY_BINARY_SENSOR = "binary_sensor.multisensor_6_low_battery_level" ENABLED_LEGACY_BINARY_SENSOR = "binary_sensor.z_wave_door_window_sensor_any" DISABLED_LEGACY_BINARY_SENSOR = "binary_sensor.multisensor_6_any" NOTIFICATION_MOTION_BINARY_SENSOR = "binary_sensor.multisensor_6_motion_detection" diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index e0485ced091..25f40e4418d 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -199,6 +199,12 @@ def climate_heatit_z_trm3_no_value_state_fixture() -> dict[str, Any]: return load_json_object_fixture("climate_heatit_z_trm3_no_value_state.json", DOMAIN) +@pytest.fixture(name="ring_keypad_state", scope="package") +def ring_keypad_state_fixture() -> dict[str, Any]: + """Load the Ring keypad state fixture data.""" + return load_json_object_fixture("ring_keypad_state.json", DOMAIN) + + @pytest.fixture(name="nortek_thermostat_state", scope="package") def nortek_thermostat_state_fixture() -> dict[str, Any]: """Load the nortek thermostat node state fixture data.""" @@ -876,6 +882,14 @@ def nortek_thermostat_removed_event_fixture(client) -> Node: return Event("node removed", event_data) +@pytest.fixture(name="ring_keypad") +def ring_keypad_fixture(client: MagicMock, ring_keypad_state: NodeDataType) -> Node: + """Mock a Ring keypad node.""" + node = Node(client, copy.deepcopy(ring_keypad_state)) + client.driver.controller.nodes[node.node_id] = node + return node + + @pytest.fixture(name="integration") async def integration_fixture( hass: HomeAssistant, diff --git a/tests/components/zwave_js/fixtures/ring_keypad_state.json b/tests/components/zwave_js/fixtures/ring_keypad_state.json new file mode 100644 index 00000000000..3d003518b6e --- /dev/null +++ b/tests/components/zwave_js/fixtures/ring_keypad_state.json @@ -0,0 +1,7543 @@ +{ + "nodeId": 4, + "index": 0, + "installerIcon": 8193, + "userIcon": 8193, + "status": 4, + "ready": true, + "isListening": false, + "isRouting": true, + "isSecure": true, + "manufacturerId": 838, + "productId": 1025, + "productType": 257, + "firmwareVersion": "1.18.0", + "zwavePlusVersion": 2, + "deviceConfig": { + "filename": "/home/dominic/Repositories/zwavejs2mqtt/store/.config-db/devices/0x0346/keypad_v2.json", + "isEmbedded": true, + "manufacturer": "Ring", + "manufacturerId": 838, + "label": "4AK1SZ", + "description": "Keypad v2", + "devices": [ + { + "productType": 257, + "productId": 769 + }, + { + "productType": 257, + "productId": 1025 + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "preferred": false, + "paramInformation": { + "_map": {} + }, + "compat": { + "disableStrictEntryControlDataValidation": true + }, + "metadata": { + "inclusion": "Classic Inclusion should be used if the controller does not support SmartStart.\n1. Initiate add flow for Security Devices in the Ring mobile application \u2013 Follow the guided add flow instructions provided in the Ring mobile application.\n2. Select add manually and enter the 5-digit DSK PIN found on the package of the Ring Alarm Keypad or the 5-digit DSK PIN found under the QR code on the device.\n3. After powering on the device, press and hold the #1 button for ~3 seconds. Release the button and the device will enter Classic inclusion mode which implements both classic inclusion with a Node Information Frame, and Network Wide Inclusion. During Classic Inclusion mode, the green Connection LED will blink three times followed by a brief pause, repeatedly. When Classic inclusion times-out, the device will blink alternating red and green a few times", + "exclusion": "1. Initiate remove 'Ring Alarm Keypad' flow in the Ring Alarm mobile application \u2013 Select the settings icon from device details page and choose 'Remove Device' to remove the device. This will place the controller into Remove or 'Z-Wave Exclusion' mode.\n2. Locate the pinhole reset button on the back of the device.\n3. With the controller in Remove (Z-Wave Exclusion) mode, use a paper clip or similar object and tap the pinhole button. The device's Connection LED turns on solid red to indicate the device was removed from the network.", + "reset": "Factory Default Instructions\n1. To restore Ring Alarm Keypad to factory default settings, locate the pinhole reset button on the device. This is found on the back of the device after removing the back bracket.\n2. Using a paperclip or similar object, insert it into the pinhole, press and hold the button down for 10 seconds.\n3. The device's Connection icon LED will rapidly blink green continuously for 10 seconds. After about 10 seconds, when the green blinking stops, release the button. The red LED will turn on solid to indicate the device was removed from the network.\nNote\nUse this procedure only in the event that the network primary controller is missing or otherwise inoperable", + "manual": "https://products.z-wavealliance.org/ProductManual/File?folder=&filename=product_documents/4150/Ring%20Alarm%20Keypad%20Zwave.pdf" + } + }, + "label": "4AK1SZ", + "interviewAttempts": 0, + "isFrequentListening": "250ms", + "maxDataRate": 100000, + "supportedDataRates": [40000, 100000], + "protocolVersion": 3, + "supportsBeaming": true, + "supportsSecurity": false, + "nodeType": 1, + "zwavePlusNodeType": 0, + "zwavePlusRoleType": 7, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing End Node" + }, + "generic": { + "key": 64, + "label": "Entry Control" + }, + "specific": { + "key": 11, + "label": "Secure Keypad" + } + }, + "interviewStage": "Complete", + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0346:0x0101:0x0401:1.18.0", + "statistics": { + "commandsTX": 1, + "commandsRX": 0, + "commandsDroppedRX": 0, + "commandsDroppedTX": 0, + "timeoutResponse": 0, + "rtt": 27.5, + "lastSeen": "2025-06-18T11:17:39.315Z", + "rssi": -54, + "lwr": { + "protocolDataRate": 2, + "repeaters": [], + "rssi": -54, + "repeaterRSSI": [] + } + }, + "highestSecurityClass": 2, + "isControllerNode": false, + "keepAwake": false, + "lastSeen": "2025-06-18T11:17:39.315Z", + "protocol": 0, + "values": [ + { + "endpoint": 0, + "commandClass": 111, + "commandClassName": "Entry Control", + "property": "keyCacheTimeout", + "propertyName": "keyCacheTimeout", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "How long the key cache must wait for additional characters", + "label": "Key cache timeout", + "min": 1, + "max": 30, + "unit": "seconds", + "stateful": true, + "secret": false + }, + "value": 5 + }, + { + "endpoint": 0, + "commandClass": 111, + "commandClassName": "Entry Control", + "property": "keyCacheSize", + "propertyName": "keyCacheSize", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Number of character that must be stored before sending", + "label": "Key cache size", + "min": 4, + "max": 10, + "stateful": true, + "secret": false + }, + "value": 8 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 1, + "propertyName": "Heartbeat Interval", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Heartbeat Interval", + "default": 70, + "min": 1, + "max": 70, + "unit": "minutes", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 70 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 2, + "propertyName": "Message Retry Attempt Limit", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Message Retry Attempt Limit", + "default": 1, + "min": 0, + "max": 5, + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 3, + "propertyName": "Delay Between Retry Attempts", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Delay Between Retry Attempts", + "default": 5, + "min": 1, + "max": 60, + "unit": "seconds", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 5 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 4, + "propertyName": "Announcement Audio Volume", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Announcement Audio Volume", + "default": 7, + "min": 0, + "max": 10, + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 7 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 5, + "propertyName": "Key Tone Volume", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Key Tone Volume", + "default": 6, + "min": 0, + "max": 10, + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 6 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 6, + "propertyName": "Siren Volume", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Siren Volume", + "default": 10, + "min": 0, + "max": 10, + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 10 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 7, + "propertyName": "Long Press Duration: Emergency Buttons", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Hold time required to capture a long press", + "label": "Long Press Duration: Emergency Buttons", + "default": 3, + "min": 2, + "max": 5, + "unit": "seconds", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 8, + "propertyName": "Long Press Duration: Number Pad", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Hold time required to capture a long press", + "label": "Long Press Duration: Number Pad", + "default": 3, + "min": 2, + "max": 5, + "unit": "seconds", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 9, + "propertyName": "Timeout: Proximity Display", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Timeout: Proximity Display", + "default": 5, + "min": 0, + "max": 30, + "unit": "seconds", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 5 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 10, + "propertyName": "Timeout: Display on Button Press", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Timeout: Display on Button Press", + "default": 5, + "min": 0, + "max": 30, + "unit": "seconds", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 5 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 11, + "propertyName": "Timeout: Display on Status Change", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Timeout: Display on Status Change", + "default": 5, + "min": 1, + "max": 30, + "unit": "seconds", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 5 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 12, + "propertyName": "Brightness: Security Mode", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Brightness: Security Mode", + "default": 100, + "min": 0, + "max": 100, + "unit": "%", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 100 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 13, + "propertyName": "Brightness: Key Backlight", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Brightness: Key Backlight", + "default": 100, + "min": 0, + "max": 100, + "unit": "%", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 100 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 14, + "propertyName": "Key Backlight Ambient Light Sensor Level", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Key Backlight Ambient Light Sensor Level", + "default": 20, + "min": 0, + "max": 100, + "unit": "%", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 20 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 15, + "propertyName": "Proximity Detection", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Proximity Detection", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "Disable", + "1": "Enable" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 16, + "propertyName": "LED Ramp Time", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "LED Ramp Time", + "default": 50, + "min": 0, + "max": 255, + "unit": "seconds", + "valueSize": 1, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 50 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 17, + "propertyName": "Battery Low Threshold", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Battery Low Threshold", + "default": 15, + "min": 0, + "max": 100, + "unit": "%", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 30 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 19, + "propertyName": "Battery Warning Threshold", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Battery Warning Threshold", + "default": 5, + "min": 0, + "max": 100, + "unit": "%", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 10 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 18, + "propertyName": "Keypad Language", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Keypad Language", + "default": 30, + "min": 0, + "max": 31, + "states": { + "0": "English", + "2": "French", + "5": "Spanish" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 20, + "propertyName": "System Security Mode Blink Duration", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "System Security Mode Blink Duration", + "default": 2, + "min": 1, + "max": 60, + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 2 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 21, + "propertyName": "Supervision Report Timeout", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Supervision Report Timeout", + "default": 10000, + "min": 500, + "max": 30000, + "unit": "ms", + "valueSize": 2, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 10000 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 22, + "propertyName": "System Security Mode Display", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Allowable range: 1-600", + "label": "System Security Mode Display", + "default": 0, + "min": 0, + "max": 601, + "states": { + "0": "Always off", + "601": "Always on" + }, + "valueSize": 2, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 1, + "propertyName": "param023_1", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 1, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 2, + "propertyName": "param023_2", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 4, + "propertyName": "param023_4", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 1, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 8, + "propertyName": "param023_8", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 16, + "propertyName": "param023_16", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 32, + "propertyName": "param023_32", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 1, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 64, + "propertyName": "param023_64", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 128, + "propertyName": "param023_128", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 256, + "propertyName": "param023_256", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 512, + "propertyName": "param023_512", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 1024, + "propertyName": "param023_1024", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 2048, + "propertyName": "param023_2048", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 4096, + "propertyName": "param023_4096", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 8192, + "propertyName": "param023_8192", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 16384, + "propertyName": "param023_16384", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 32768, + "propertyName": "param023_32768", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 65536, + "propertyName": "param023_65536", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 131072, + "propertyName": "param023_131072", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 262144, + "propertyName": "param023_262144", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 524288, + "propertyName": "param023_524288", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 1048576, + "propertyName": "param023_1048576", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 2097152, + "propertyName": "param023_2097152", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 4194304, + "propertyName": "param023_4194304", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 8388608, + "propertyName": "param023_8388608", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 16777216, + "propertyName": "param023_16777216", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 33554432, + "propertyName": "param023_33554432", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 67108864, + "propertyName": "param023_67108864", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 134217728, + "propertyName": "param023_134217728", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 268435456, + "propertyName": "param023_268435456", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 536870912, + "propertyName": "param023_536870912", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 1073741824, + "propertyName": "param023_1073741824", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 2147483648, + "propertyName": "param023_2147483648", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 24, + "propertyName": "Calibrate Speaker", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Calibrate Speaker", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "Disable", + "1": "Enable" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 26, + "propertyName": "Motion Sensor Timeout", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Motion Sensor Timeout", + "default": 3, + "min": 0, + "max": 60, + "unit": "seconds", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 2 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 25, + "propertyName": "Z-Wave Sleep Timeout", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "", + "label": "Z-Wave Sleep Timeout", + "default": 10, + "min": 0, + "max": 15, + "valueSize": 1, + "format": 1, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 10 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyName": "Languages Supported Report", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "description": "This parameter reports a bitmask of supported languages", + "label": "Languages Supported Report", + "default": 37, + "min": 0, + "max": 4294967295, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Home Security", + "propertyKey": "Motion sensor status", + "propertyName": "Home Security", + "propertyKeyName": "Motion sensor status", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Motion sensor status", + "ccSpecific": { + "notificationType": 7 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "8": "Motion detection" + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Power Management", + "propertyKey": "Power status", + "propertyName": "Power Management", + "propertyKeyName": "Power status", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Power status", + "ccSpecific": { + "notificationType": 8 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "1": "Power has been applied" + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "System", + "propertyKey": "Software status", + "propertyName": "System", + "propertyKeyName": "Software status", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Software status", + "ccSpecific": { + "notificationType": 9 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "4": "System software failure (with failure code)" + }, + "stateful": true, + "secret": false + }, + "value": 4 + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmType", + "propertyName": "alarmType", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Alarm Type", + "min": 0, + "max": 255, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmLevel", + "propertyName": "alarmLevel", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Alarm Level", + "min": 0, + "max": 255, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Power Management", + "propertyKey": "Mains status", + "propertyName": "Power Management", + "propertyKeyName": "Mains status", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Mains status", + "ccSpecific": { + "notificationType": 8 + }, + "min": 0, + "max": 255, + "states": { + "2": "AC mains disconnected", + "3": "AC mains re-connected" + }, + "stateful": true, + "secret": false + }, + "value": 2 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product ID", + "min": 0, + "max": 65535, + "stateful": true, + "secret": false + }, + "value": 1025 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product type", + "min": 0, + "max": 65535, + "stateful": true, + "secret": false + }, + "value": 257 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Manufacturer ID", + "min": 0, + "max": 65535, + "stateful": true, + "secret": false + }, + "value": 838 + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "disconnected", + "propertyName": "disconnected", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Battery is disconnected", + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "rechargeOrReplace", + "propertyName": "rechargeOrReplace", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Recharge or replace", + "min": 0, + "max": 255, + "states": { + "0": "No", + "1": "Soon", + "2": "Now" + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "lowFluid", + "propertyName": "lowFluid", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Fluid is low", + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "overheating", + "propertyName": "overheating", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Overheating", + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "backup", + "propertyName": "backup", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Used as backup", + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "rechargeable", + "propertyName": "rechargeable", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Rechargeable", + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "chargingStatus", + "propertyName": "chargingStatus", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Charging status", + "min": 0, + "max": 255, + "states": { + "0": "Discharging", + "1": "Charging", + "2": "Maintaining" + }, + "stateful": true, + "secret": false + }, + "value": 2 + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "isLow", + "propertyName": "isLow", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Low battery level", + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "level", + "propertyName": "level", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Battery level", + "min": 0, + "max": 100, + "unit": "%", + "stateful": true, + "secret": false + }, + "value": 100 + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "temperature", + "propertyName": "temperature", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Temperature", + "min": -128, + "max": 127, + "unit": "\u00b0C", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "maximumCapacity", + "propertyName": "maximumCapacity", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Maximum capacity", + "min": 0, + "max": 100, + "unit": "%", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version", + "stateful": true, + "secret": false + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 3, + "metadata": { + "type": "string[]", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions", + "stateful": true, + "secret": false + }, + "value": ["1.18", "1.1"] + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version", + "stateful": true, + "secret": false + }, + "value": "7.12" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Library type", + "states": { + "0": "Unknown", + "1": "Static Controller", + "2": "Controller", + "3": "Enhanced Slave", + "4": "Slave", + "5": "Installer", + "6": "Routing Slave", + "7": "Bridge Controller", + "8": "Device under Test", + "9": "N/A", + "10": "AV Remote", + "11": "AV Device" + }, + "stateful": true, + "secret": false + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationBuildNumber", + "propertyName": "applicationBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Application build number", + "stateful": true, + "secret": false + }, + "value": 28 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationVersion", + "propertyName": "applicationVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Application version", + "stateful": true, + "secret": false + }, + "value": "1.18.0" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "zWaveProtocolBuildNumber", + "propertyName": "zWaveProtocolBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol build number", + "stateful": true, + "secret": false + }, + "value": 48 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "zWaveProtocolVersion", + "propertyName": "zWaveProtocolVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version", + "stateful": true, + "secret": false + }, + "value": "7.12.4" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hostInterfaceBuildNumber", + "propertyName": "hostInterfaceBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Serial API build number", + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hostInterfaceVersion", + "propertyName": "hostInterfaceVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Serial API version", + "stateful": true, + "secret": false + }, + "value": "unused" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationFrameworkBuildNumber", + "propertyName": "applicationFrameworkBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave application framework API build number", + "stateful": true, + "secret": false + }, + "value": 48 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationFrameworkAPIVersion", + "propertyName": "applicationFrameworkAPIVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave application framework API version", + "stateful": true, + "secret": false + }, + "value": "1.18.0" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "sdkVersion", + "propertyName": "sdkVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "SDK version", + "stateful": true, + "secret": false + }, + "value": "7.12.4" + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 0, + "propertyKey": 1, + "propertyName": "0", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0 (default) - Multilevel", + "ccSpecific": { + "indicatorId": 0, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 0, + "propertyKey": 3, + "propertyName": "0", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0 (default) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 0, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 0, + "propertyKey": 4, + "propertyName": "0", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0 (default) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 0, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 0, + "propertyKey": 5, + "propertyName": "0", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0 (default) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 0, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 0, + "propertyKey": 7, + "propertyName": "0", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0 (default) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 0, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 0, + "propertyKey": 9, + "propertyName": "0", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0 (default) - Sound level", + "ccSpecific": { + "indicatorId": 0, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 0, + "propertyKey": 8, + "propertyName": "0", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0 (default) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 0, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 0, + "propertyKey": 6, + "propertyName": "0", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0 (default) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 0, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": "timeout", + "propertyName": "timeout", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "Timeout", + "stateful": true, + "secret": false + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 3, + "propertyKey": 1, + "propertyName": "Ready", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x03 (Ready) - Multilevel", + "ccSpecific": { + "indicatorId": 3, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 6 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 3, + "propertyKey": 3, + "propertyName": "Ready", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x03 (Ready) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 3, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 3, + "propertyKey": 4, + "propertyName": "Ready", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x03 (Ready) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 3, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 3, + "propertyKey": 5, + "propertyName": "Ready", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x03 (Ready) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 3, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 3, + "propertyKey": 7, + "propertyName": "Ready", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x03 (Ready) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 3, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 4 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 3, + "propertyKey": 9, + "propertyName": "Ready", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x03 (Ready) - Sound level", + "ccSpecific": { + "indicatorId": 3, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 3, + "propertyKey": 8, + "propertyName": "Ready", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x03 (Ready) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 3, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 3, + "propertyKey": 6, + "propertyName": "Ready", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x03 (Ready) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 3, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 2, + "propertyKey": 1, + "propertyName": "Not armed / disarmed", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x02 (Not armed / disarmed) - Multilevel", + "ccSpecific": { + "indicatorId": 2, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 2, + "propertyKey": 3, + "propertyName": "Not armed / disarmed", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x02 (Not armed / disarmed) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 2, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 2, + "propertyKey": 4, + "propertyName": "Not armed / disarmed", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x02 (Not armed / disarmed) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 2, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 2, + "propertyKey": 5, + "propertyName": "Not armed / disarmed", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x02 (Not armed / disarmed) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 2, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 2, + "propertyKey": 7, + "propertyName": "Not armed / disarmed", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x02 (Not armed / disarmed) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 2, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 2, + "propertyKey": 9, + "propertyName": "Not armed / disarmed", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x02 (Not armed / disarmed) - Sound level", + "ccSpecific": { + "indicatorId": 2, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 2, + "propertyKey": 8, + "propertyName": "Not armed / disarmed", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x02 (Not armed / disarmed) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 2, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 2, + "propertyKey": 6, + "propertyName": "Not armed / disarmed", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x02 (Not armed / disarmed) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 2, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 9, + "propertyKey": 1, + "propertyName": "Code not accepted", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x09 (Code not accepted) - Multilevel", + "ccSpecific": { + "indicatorId": 9, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 9, + "propertyKey": 3, + "propertyName": "Code not accepted", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x09 (Code not accepted) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 9, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 9, + "propertyKey": 4, + "propertyName": "Code not accepted", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x09 (Code not accepted) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 9, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 9, + "propertyKey": 5, + "propertyName": "Code not accepted", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x09 (Code not accepted) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 9, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 9, + "propertyKey": 7, + "propertyName": "Code not accepted", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x09 (Code not accepted) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 9, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 9, + "propertyKey": 9, + "propertyName": "Code not accepted", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x09 (Code not accepted) - Sound level", + "ccSpecific": { + "indicatorId": 9, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 9, + "propertyKey": 8, + "propertyName": "Code not accepted", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x09 (Code not accepted) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 9, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 9, + "propertyKey": 6, + "propertyName": "Code not accepted", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x09 (Code not accepted) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 9, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 10, + "propertyKey": 1, + "propertyName": "Armed Stay", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x0a (Armed Stay) - Multilevel", + "ccSpecific": { + "indicatorId": 10, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 10, + "propertyKey": 3, + "propertyName": "Armed Stay", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x0a (Armed Stay) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 10, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 10, + "propertyKey": 4, + "propertyName": "Armed Stay", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x0a (Armed Stay) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 10, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 10, + "propertyKey": 5, + "propertyName": "Armed Stay", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x0a (Armed Stay) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 10, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 10, + "propertyKey": 7, + "propertyName": "Armed Stay", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x0a (Armed Stay) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 10, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 10, + "propertyKey": 9, + "propertyName": "Armed Stay", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x0a (Armed Stay) - Sound level", + "ccSpecific": { + "indicatorId": 10, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 10, + "propertyKey": 8, + "propertyName": "Armed Stay", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x0a (Armed Stay) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 10, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 10, + "propertyKey": 6, + "propertyName": "Armed Stay", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x0a (Armed Stay) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 10, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 11, + "propertyKey": 1, + "propertyName": "Armed Away", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x0b (Armed Away) - Multilevel", + "ccSpecific": { + "indicatorId": 11, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 11, + "propertyKey": 3, + "propertyName": "Armed Away", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x0b (Armed Away) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 11, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 11, + "propertyKey": 4, + "propertyName": "Armed Away", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x0b (Armed Away) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 11, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 11, + "propertyKey": 5, + "propertyName": "Armed Away", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x0b (Armed Away) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 11, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 11, + "propertyKey": 7, + "propertyName": "Armed Away", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x0b (Armed Away) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 11, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 11, + "propertyKey": 9, + "propertyName": "Armed Away", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x0b (Armed Away) - Sound level", + "ccSpecific": { + "indicatorId": 11, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 11, + "propertyKey": 8, + "propertyName": "Armed Away", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x0b (Armed Away) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 11, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 11, + "propertyKey": 6, + "propertyName": "Armed Away", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x0b (Armed Away) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 11, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 12, + "propertyKey": 1, + "propertyName": "Alarming", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x0c (Alarming) - Multilevel", + "ccSpecific": { + "indicatorId": 12, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 12, + "propertyKey": 3, + "propertyName": "Alarming", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x0c (Alarming) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 12, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 12, + "propertyKey": 4, + "propertyName": "Alarming", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x0c (Alarming) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 12, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 12, + "propertyKey": 5, + "propertyName": "Alarming", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x0c (Alarming) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 12, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 12, + "propertyKey": 7, + "propertyName": "Alarming", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x0c (Alarming) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 12, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 12, + "propertyKey": 9, + "propertyName": "Alarming", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x0c (Alarming) - Sound level", + "ccSpecific": { + "indicatorId": 12, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 12, + "propertyKey": 8, + "propertyName": "Alarming", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x0c (Alarming) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 12, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 12, + "propertyKey": 6, + "propertyName": "Alarming", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x0c (Alarming) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 12, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 13, + "propertyKey": 1, + "propertyName": "Alarming: Burglar", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x0d (Alarming: Burglar) - Multilevel", + "ccSpecific": { + "indicatorId": 13, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 13, + "propertyKey": 3, + "propertyName": "Alarming: Burglar", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x0d (Alarming: Burglar) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 13, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 13, + "propertyKey": 4, + "propertyName": "Alarming: Burglar", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x0d (Alarming: Burglar) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 13, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 13, + "propertyKey": 5, + "propertyName": "Alarming: Burglar", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x0d (Alarming: Burglar) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 13, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 13, + "propertyKey": 7, + "propertyName": "Alarming: Burglar", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x0d (Alarming: Burglar) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 13, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 13, + "propertyKey": 9, + "propertyName": "Alarming: Burglar", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x0d (Alarming: Burglar) - Sound level", + "ccSpecific": { + "indicatorId": 13, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 13, + "propertyKey": 8, + "propertyName": "Alarming: Burglar", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x0d (Alarming: Burglar) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 13, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 13, + "propertyKey": 6, + "propertyName": "Alarming: Burglar", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x0d (Alarming: Burglar) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 13, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 14, + "propertyKey": 1, + "propertyName": "Alarming: Smoke / Fire", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x0e (Alarming: Smoke / Fire) - Multilevel", + "ccSpecific": { + "indicatorId": 14, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 14, + "propertyKey": 3, + "propertyName": "Alarming: Smoke / Fire", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x0e (Alarming: Smoke / Fire) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 14, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 14, + "propertyKey": 4, + "propertyName": "Alarming: Smoke / Fire", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x0e (Alarming: Smoke / Fire) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 14, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 14, + "propertyKey": 5, + "propertyName": "Alarming: Smoke / Fire", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x0e (Alarming: Smoke / Fire) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 14, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 14, + "propertyKey": 7, + "propertyName": "Alarming: Smoke / Fire", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x0e (Alarming: Smoke / Fire) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 14, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 14, + "propertyKey": 9, + "propertyName": "Alarming: Smoke / Fire", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x0e (Alarming: Smoke / Fire) - Sound level", + "ccSpecific": { + "indicatorId": 14, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 14, + "propertyKey": 8, + "propertyName": "Alarming: Smoke / Fire", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x0e (Alarming: Smoke / Fire) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 14, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 14, + "propertyKey": 6, + "propertyName": "Alarming: Smoke / Fire", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x0e (Alarming: Smoke / Fire) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 14, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 15, + "propertyKey": 1, + "propertyName": "Alarming: Carbon Monoxide", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x0f (Alarming: Carbon Monoxide) - Multilevel", + "ccSpecific": { + "indicatorId": 15, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 15, + "propertyKey": 3, + "propertyName": "Alarming: Carbon Monoxide", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x0f (Alarming: Carbon Monoxide) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 15, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 15, + "propertyKey": 4, + "propertyName": "Alarming: Carbon Monoxide", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x0f (Alarming: Carbon Monoxide) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 15, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 15, + "propertyKey": 5, + "propertyName": "Alarming: Carbon Monoxide", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x0f (Alarming: Carbon Monoxide) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 15, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 15, + "propertyKey": 7, + "propertyName": "Alarming: Carbon Monoxide", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x0f (Alarming: Carbon Monoxide) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 15, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 15, + "propertyKey": 9, + "propertyName": "Alarming: Carbon Monoxide", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x0f (Alarming: Carbon Monoxide) - Sound level", + "ccSpecific": { + "indicatorId": 15, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 15, + "propertyKey": 8, + "propertyName": "Alarming: Carbon Monoxide", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x0f (Alarming: Carbon Monoxide) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 15, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 15, + "propertyKey": 6, + "propertyName": "Alarming: Carbon Monoxide", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x0f (Alarming: Carbon Monoxide) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 15, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 16, + "propertyKey": 1, + "propertyName": "Bypass challenge", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x10 (Bypass challenge) - Multilevel", + "ccSpecific": { + "indicatorId": 16, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 16, + "propertyKey": 3, + "propertyName": "Bypass challenge", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x10 (Bypass challenge) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 16, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 16, + "propertyKey": 4, + "propertyName": "Bypass challenge", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x10 (Bypass challenge) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 16, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 16, + "propertyKey": 5, + "propertyName": "Bypass challenge", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x10 (Bypass challenge) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 16, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 16, + "propertyKey": 7, + "propertyName": "Bypass challenge", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x10 (Bypass challenge) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 16, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 16, + "propertyKey": 9, + "propertyName": "Bypass challenge", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x10 (Bypass challenge) - Sound level", + "ccSpecific": { + "indicatorId": 16, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 16, + "propertyKey": 8, + "propertyName": "Bypass challenge", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x10 (Bypass challenge) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 16, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 16, + "propertyKey": 6, + "propertyName": "Bypass challenge", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x10 (Bypass challenge) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 16, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 17, + "propertyKey": 1, + "propertyName": "Entry Delay", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x11 (Entry Delay) - Multilevel", + "ccSpecific": { + "indicatorId": 17, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 17, + "propertyKey": 3, + "propertyName": "Entry Delay", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x11 (Entry Delay) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 17, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 17, + "propertyKey": 4, + "propertyName": "Entry Delay", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x11 (Entry Delay) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 17, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 17, + "propertyKey": 5, + "propertyName": "Entry Delay", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x11 (Entry Delay) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 17, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 17, + "propertyKey": 7, + "propertyName": "Entry Delay", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x11 (Entry Delay) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 17, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 17, + "propertyKey": 9, + "propertyName": "Entry Delay", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x11 (Entry Delay) - Sound level", + "ccSpecific": { + "indicatorId": 17, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 17, + "propertyKey": 8, + "propertyName": "Entry Delay", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x11 (Entry Delay) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 17, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 17, + "propertyKey": 6, + "propertyName": "Entry Delay", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x11 (Entry Delay) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 17, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 18, + "propertyKey": 1, + "propertyName": "Exit Delay", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x12 (Exit Delay) - Multilevel", + "ccSpecific": { + "indicatorId": 18, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 18, + "propertyKey": 3, + "propertyName": "Exit Delay", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x12 (Exit Delay) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 18, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 18, + "propertyKey": 4, + "propertyName": "Exit Delay", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x12 (Exit Delay) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 18, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 18, + "propertyKey": 5, + "propertyName": "Exit Delay", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x12 (Exit Delay) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 18, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 18, + "propertyKey": 7, + "propertyName": "Exit Delay", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x12 (Exit Delay) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 18, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 18, + "propertyKey": 9, + "propertyName": "Exit Delay", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x12 (Exit Delay) - Sound level", + "ccSpecific": { + "indicatorId": 18, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 18, + "propertyKey": 8, + "propertyName": "Exit Delay", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x12 (Exit Delay) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 18, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 18, + "propertyKey": 6, + "propertyName": "Exit Delay", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x12 (Exit Delay) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 18, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 19, + "propertyKey": 1, + "propertyName": "Alarming: Medical", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x13 (Alarming: Medical) - Multilevel", + "ccSpecific": { + "indicatorId": 19, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 19, + "propertyKey": 3, + "propertyName": "Alarming: Medical", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x13 (Alarming: Medical) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 19, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 19, + "propertyKey": 4, + "propertyName": "Alarming: Medical", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x13 (Alarming: Medical) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 19, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 19, + "propertyKey": 5, + "propertyName": "Alarming: Medical", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x13 (Alarming: Medical) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 19, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 19, + "propertyKey": 7, + "propertyName": "Alarming: Medical", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x13 (Alarming: Medical) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 19, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 19, + "propertyKey": 9, + "propertyName": "Alarming: Medical", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x13 (Alarming: Medical) - Sound level", + "ccSpecific": { + "indicatorId": 19, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 19, + "propertyKey": 8, + "propertyName": "Alarming: Medical", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x13 (Alarming: Medical) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 19, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 19, + "propertyKey": 6, + "propertyName": "Alarming: Medical", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x13 (Alarming: Medical) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 19, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 80, + "propertyKey": 1, + "propertyName": "Node Identify", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x50 (Node Identify) - Multilevel", + "ccSpecific": { + "indicatorId": 80, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 80, + "propertyKey": 3, + "propertyName": "Node Identify", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x50 (Node Identify) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 80, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 80, + "propertyKey": 4, + "propertyName": "Node Identify", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x50 (Node Identify) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 80, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 80, + "propertyKey": 5, + "propertyName": "Node Identify", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x50 (Node Identify) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 80, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 80, + "propertyKey": 7, + "propertyName": "Node Identify", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x50 (Node Identify) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 80, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 80, + "propertyKey": 9, + "propertyName": "Node Identify", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x50 (Node Identify) - Sound level", + "ccSpecific": { + "indicatorId": 80, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 80, + "propertyKey": 8, + "propertyName": "Node Identify", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x50 (Node Identify) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 80, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 80, + "propertyKey": 6, + "propertyName": "Node Identify", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x50 (Node Identify) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 80, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 96, + "propertyKey": 1, + "propertyName": "Generic event sound notification 1", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x60 (Generic event sound notification 1) - Multilevel", + "ccSpecific": { + "indicatorId": 96, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 96, + "propertyKey": 3, + "propertyName": "Generic event sound notification 1", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x60 (Generic event sound notification 1) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 96, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 96, + "propertyKey": 4, + "propertyName": "Generic event sound notification 1", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x60 (Generic event sound notification 1) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 96, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 96, + "propertyKey": 5, + "propertyName": "Generic event sound notification 1", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x60 (Generic event sound notification 1) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 96, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 96, + "propertyKey": 7, + "propertyName": "Generic event sound notification 1", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x60 (Generic event sound notification 1) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 96, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 96, + "propertyKey": 9, + "propertyName": "Generic event sound notification 1", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x60 (Generic event sound notification 1) - Sound level", + "ccSpecific": { + "indicatorId": 96, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 96, + "propertyKey": 8, + "propertyName": "Generic event sound notification 1", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x60 (Generic event sound notification 1) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 96, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 96, + "propertyKey": 6, + "propertyName": "Generic event sound notification 1", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x60 (Generic event sound notification 1) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 96, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 97, + "propertyKey": 1, + "propertyName": "Generic event sound notification 2", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x61 (Generic event sound notification 2) - Multilevel", + "ccSpecific": { + "indicatorId": 97, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 97, + "propertyKey": 3, + "propertyName": "Generic event sound notification 2", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x61 (Generic event sound notification 2) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 97, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 97, + "propertyKey": 4, + "propertyName": "Generic event sound notification 2", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x61 (Generic event sound notification 2) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 97, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 97, + "propertyKey": 5, + "propertyName": "Generic event sound notification 2", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x61 (Generic event sound notification 2) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 97, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 97, + "propertyKey": 7, + "propertyName": "Generic event sound notification 2", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x61 (Generic event sound notification 2) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 97, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 97, + "propertyKey": 9, + "propertyName": "Generic event sound notification 2", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x61 (Generic event sound notification 2) - Sound level", + "ccSpecific": { + "indicatorId": 97, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 97, + "propertyKey": 8, + "propertyName": "Generic event sound notification 2", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x61 (Generic event sound notification 2) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 97, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 97, + "propertyKey": 6, + "propertyName": "Generic event sound notification 2", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x61 (Generic event sound notification 2) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 97, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 98, + "propertyKey": 1, + "propertyName": "Generic event sound notification 3", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x62 (Generic event sound notification 3) - Multilevel", + "ccSpecific": { + "indicatorId": 98, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 98, + "propertyKey": 3, + "propertyName": "Generic event sound notification 3", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x62 (Generic event sound notification 3) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 98, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 98, + "propertyKey": 4, + "propertyName": "Generic event sound notification 3", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x62 (Generic event sound notification 3) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 98, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 98, + "propertyKey": 5, + "propertyName": "Generic event sound notification 3", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x62 (Generic event sound notification 3) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 98, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 98, + "propertyKey": 7, + "propertyName": "Generic event sound notification 3", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x62 (Generic event sound notification 3) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 98, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 98, + "propertyKey": 9, + "propertyName": "Generic event sound notification 3", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x62 (Generic event sound notification 3) - Sound level", + "ccSpecific": { + "indicatorId": 98, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 98, + "propertyKey": 8, + "propertyName": "Generic event sound notification 3", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x62 (Generic event sound notification 3) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 98, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 98, + "propertyKey": 6, + "propertyName": "Generic event sound notification 3", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x62 (Generic event sound notification 3) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 98, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 99, + "propertyKey": 1, + "propertyName": "Generic event sound notification 4", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x63 (Generic event sound notification 4) - Multilevel", + "ccSpecific": { + "indicatorId": 99, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 99, + "propertyKey": 3, + "propertyName": "Generic event sound notification 4", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x63 (Generic event sound notification 4) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 99, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 99, + "propertyKey": 4, + "propertyName": "Generic event sound notification 4", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x63 (Generic event sound notification 4) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 99, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 99, + "propertyKey": 5, + "propertyName": "Generic event sound notification 4", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x63 (Generic event sound notification 4) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 99, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 99, + "propertyKey": 7, + "propertyName": "Generic event sound notification 4", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x63 (Generic event sound notification 4) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 99, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 99, + "propertyKey": 9, + "propertyName": "Generic event sound notification 4", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x63 (Generic event sound notification 4) - Sound level", + "ccSpecific": { + "indicatorId": 99, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 99, + "propertyKey": 8, + "propertyName": "Generic event sound notification 4", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x63 (Generic event sound notification 4) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 99, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 99, + "propertyKey": 6, + "propertyName": "Generic event sound notification 4", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x63 (Generic event sound notification 4) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 99, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 100, + "propertyKey": 1, + "propertyName": "Generic event sound notification 5", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x64 (Generic event sound notification 5) - Multilevel", + "ccSpecific": { + "indicatorId": 100, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 100, + "propertyKey": 3, + "propertyName": "Generic event sound notification 5", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x64 (Generic event sound notification 5) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 100, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 100, + "propertyKey": 4, + "propertyName": "Generic event sound notification 5", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x64 (Generic event sound notification 5) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 100, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 100, + "propertyKey": 5, + "propertyName": "Generic event sound notification 5", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x64 (Generic event sound notification 5) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 100, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 100, + "propertyKey": 7, + "propertyName": "Generic event sound notification 5", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x64 (Generic event sound notification 5) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 100, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 100, + "propertyKey": 9, + "propertyName": "Generic event sound notification 5", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x64 (Generic event sound notification 5) - Sound level", + "ccSpecific": { + "indicatorId": 100, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 100, + "propertyKey": 8, + "propertyName": "Generic event sound notification 5", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x64 (Generic event sound notification 5) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 100, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 100, + "propertyKey": 6, + "propertyName": "Generic event sound notification 5", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x64 (Generic event sound notification 5) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 100, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 20, + "propertyKey": 1, + "propertyName": "Alarming: Freeze warning", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x14 (Alarming: Freeze warning) - Multilevel", + "ccSpecific": { + "indicatorId": 20, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 20, + "propertyKey": 3, + "propertyName": "Alarming: Freeze warning", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x14 (Alarming: Freeze warning) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 20, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 20, + "propertyKey": 4, + "propertyName": "Alarming: Freeze warning", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x14 (Alarming: Freeze warning) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 20, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 20, + "propertyKey": 5, + "propertyName": "Alarming: Freeze warning", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x14 (Alarming: Freeze warning) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 20, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 20, + "propertyKey": 7, + "propertyName": "Alarming: Freeze warning", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x14 (Alarming: Freeze warning) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 20, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 20, + "propertyKey": 9, + "propertyName": "Alarming: Freeze warning", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x14 (Alarming: Freeze warning) - Sound level", + "ccSpecific": { + "indicatorId": 20, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 20, + "propertyKey": 8, + "propertyName": "Alarming: Freeze warning", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x14 (Alarming: Freeze warning) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 20, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 20, + "propertyKey": 6, + "propertyName": "Alarming: Freeze warning", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x14 (Alarming: Freeze warning) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 20, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 21, + "propertyKey": 1, + "propertyName": "Alarming: Water leak", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x15 (Alarming: Water leak) - Multilevel", + "ccSpecific": { + "indicatorId": 21, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 21, + "propertyKey": 3, + "propertyName": "Alarming: Water leak", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x15 (Alarming: Water leak) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 21, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 21, + "propertyKey": 4, + "propertyName": "Alarming: Water leak", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x15 (Alarming: Water leak) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 21, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 21, + "propertyKey": 5, + "propertyName": "Alarming: Water leak", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x15 (Alarming: Water leak) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 21, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 21, + "propertyKey": 7, + "propertyName": "Alarming: Water leak", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x15 (Alarming: Water leak) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 21, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 21, + "propertyKey": 9, + "propertyName": "Alarming: Water leak", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x15 (Alarming: Water leak) - Sound level", + "ccSpecific": { + "indicatorId": 21, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 21, + "propertyKey": 8, + "propertyName": "Alarming: Water leak", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x15 (Alarming: Water leak) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 21, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 21, + "propertyKey": 6, + "propertyName": "Alarming: Water leak", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x15 (Alarming: Water leak) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 21, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 129, + "propertyKey": 1, + "propertyName": "Manufacturer defined 2", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x81 (Manufacturer defined 2) - Multilevel", + "ccSpecific": { + "indicatorId": 129, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 129, + "propertyKey": 3, + "propertyName": "Manufacturer defined 2", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x81 (Manufacturer defined 2) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 129, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 129, + "propertyKey": 4, + "propertyName": "Manufacturer defined 2", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x81 (Manufacturer defined 2) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 129, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 129, + "propertyKey": 5, + "propertyName": "Manufacturer defined 2", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x81 (Manufacturer defined 2) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 129, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 129, + "propertyKey": 7, + "propertyName": "Manufacturer defined 2", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x81 (Manufacturer defined 2) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 129, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 129, + "propertyKey": 9, + "propertyName": "Manufacturer defined 2", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x81 (Manufacturer defined 2) - Sound level", + "ccSpecific": { + "indicatorId": 129, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 129, + "propertyKey": 8, + "propertyName": "Manufacturer defined 2", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x81 (Manufacturer defined 2) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 129, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 129, + "propertyKey": 6, + "propertyName": "Manufacturer defined 2", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x81 (Manufacturer defined 2) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 129, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 130, + "propertyKey": 1, + "propertyName": "Manufacturer defined 3", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x82 (Manufacturer defined 3) - Multilevel", + "ccSpecific": { + "indicatorId": 130, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 130, + "propertyKey": 3, + "propertyName": "Manufacturer defined 3", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x82 (Manufacturer defined 3) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 130, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 130, + "propertyKey": 4, + "propertyName": "Manufacturer defined 3", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x82 (Manufacturer defined 3) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 130, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 130, + "propertyKey": 5, + "propertyName": "Manufacturer defined 3", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x82 (Manufacturer defined 3) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 130, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 130, + "propertyKey": 7, + "propertyName": "Manufacturer defined 3", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x82 (Manufacturer defined 3) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 130, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 130, + "propertyKey": 9, + "propertyName": "Manufacturer defined 3", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x82 (Manufacturer defined 3) - Sound level", + "ccSpecific": { + "indicatorId": 130, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 130, + "propertyKey": 8, + "propertyName": "Manufacturer defined 3", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x82 (Manufacturer defined 3) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 130, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 130, + "propertyKey": 6, + "propertyName": "Manufacturer defined 3", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x82 (Manufacturer defined 3) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 130, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 131, + "propertyKey": 1, + "propertyName": "Manufacturer defined 4", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x83 (Manufacturer defined 4) - Multilevel", + "ccSpecific": { + "indicatorId": 131, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 131, + "propertyKey": 3, + "propertyName": "Manufacturer defined 4", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x83 (Manufacturer defined 4) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 131, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 131, + "propertyKey": 4, + "propertyName": "Manufacturer defined 4", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x83 (Manufacturer defined 4) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 131, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 131, + "propertyKey": 5, + "propertyName": "Manufacturer defined 4", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x83 (Manufacturer defined 4) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 131, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 131, + "propertyKey": 7, + "propertyName": "Manufacturer defined 4", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x83 (Manufacturer defined 4) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 131, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 131, + "propertyKey": 9, + "propertyName": "Manufacturer defined 4", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x83 (Manufacturer defined 4) - Sound level", + "ccSpecific": { + "indicatorId": 131, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 131, + "propertyKey": 8, + "propertyName": "Manufacturer defined 4", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x83 (Manufacturer defined 4) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 131, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 131, + "propertyKey": 6, + "propertyName": "Manufacturer defined 4", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x83 (Manufacturer defined 4) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 131, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 132, + "propertyKey": 1, + "propertyName": "Manufacturer defined 5", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x84 (Manufacturer defined 5) - Multilevel", + "ccSpecific": { + "indicatorId": 132, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 132, + "propertyKey": 3, + "propertyName": "Manufacturer defined 5", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x84 (Manufacturer defined 5) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 132, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 132, + "propertyKey": 4, + "propertyName": "Manufacturer defined 5", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x84 (Manufacturer defined 5) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 132, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 132, + "propertyKey": 5, + "propertyName": "Manufacturer defined 5", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x84 (Manufacturer defined 5) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 132, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 132, + "propertyKey": 7, + "propertyName": "Manufacturer defined 5", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x84 (Manufacturer defined 5) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 132, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 132, + "propertyKey": 9, + "propertyName": "Manufacturer defined 5", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x84 (Manufacturer defined 5) - Sound level", + "ccSpecific": { + "indicatorId": 132, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 132, + "propertyKey": 8, + "propertyName": "Manufacturer defined 5", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x84 (Manufacturer defined 5) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 132, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 132, + "propertyKey": 6, + "propertyName": "Manufacturer defined 5", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x84 (Manufacturer defined 5) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 132, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 133, + "propertyKey": 1, + "propertyName": "Manufacturer defined 6", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x85 (Manufacturer defined 6) - Multilevel", + "ccSpecific": { + "indicatorId": 133, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 133, + "propertyKey": 3, + "propertyName": "Manufacturer defined 6", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x85 (Manufacturer defined 6) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 133, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 133, + "propertyKey": 4, + "propertyName": "Manufacturer defined 6", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x85 (Manufacturer defined 6) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 133, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 133, + "propertyKey": 5, + "propertyName": "Manufacturer defined 6", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x85 (Manufacturer defined 6) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 133, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 133, + "propertyKey": 7, + "propertyName": "Manufacturer defined 6", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x85 (Manufacturer defined 6) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 133, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 133, + "propertyKey": 9, + "propertyName": "Manufacturer defined 6", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x85 (Manufacturer defined 6) - Sound level", + "ccSpecific": { + "indicatorId": 133, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 133, + "propertyKey": 8, + "propertyName": "Manufacturer defined 6", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x85 (Manufacturer defined 6) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 133, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 133, + "propertyKey": 6, + "propertyName": "Manufacturer defined 6", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x85 (Manufacturer defined 6) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 133, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": "value", + "propertyName": "value", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Indicator value", + "ccSpecific": { + "indicatorId": 0 + }, + "min": 0, + "max": 255, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": "identify", + "propertyName": "identify", + "ccVersion": 3, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Identify", + "states": { + "true": "Identify" + }, + "stateful": true, + "secret": false + } + } + ], + "endpoints": [ + { + "nodeId": 4, + "index": 0, + "installerIcon": 8193, + "userIcon": 8193, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing End Node" + }, + "generic": { + "key": 64, + "label": "Entry Control" + }, + "specific": { + "key": 11, + "label": "Secure Keypad" + } + }, + "commandClasses": [ + { + "id": 159, + "name": "Security 2", + "version": 1, + "isSecure": true + }, + { + "id": 89, + "name": "Association Group Information", + "version": 3, + "isSecure": true + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": true + }, + { + "id": 128, + "name": "Battery", + "version": 2, + "isSecure": true + }, + { + "id": 112, + "name": "Configuration", + "version": 4, + "isSecure": true + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": true + }, + { + "id": 111, + "name": "Entry Control", + "version": 1, + "isSecure": true + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 5, + "isSecure": true + }, + { + "id": 135, + "name": "Indicator", + "version": 3, + "isSecure": true + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": true + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": true + }, + { + "id": 113, + "name": "Notification", + "version": 8, + "isSecure": true + }, + { + "id": 115, + "name": "Powerlevel", + "version": 1, + "isSecure": true + }, + { + "id": 134, + "name": "Version", + "version": 3, + "isSecure": true + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 85, + "name": "Transport Service", + "version": 2, + "isSecure": false + } + ] + } + ] +} diff --git a/tests/components/zwave_js/test_binary_sensor.py b/tests/components/zwave_js/test_binary_sensor.py index 93ac52f9041..5dfbb0f5bd8 100644 --- a/tests/components/zwave_js/test_binary_sensor.py +++ b/tests/components/zwave_js/test_binary_sensor.py @@ -1,10 +1,13 @@ """Test the Z-Wave JS binary sensor platform.""" +from datetime import timedelta + import pytest from zwave_js_server.event import Event from zwave_js_server.model.node import Node from homeassistant.components.binary_sensor import BinarySensorDeviceClass +from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY from homeassistant.const import ( ATTR_DEVICE_CLASS, STATE_OFF, @@ -15,17 +18,17 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from homeassistant.util import dt as dt_util from .common import ( DISABLED_LEGACY_BINARY_SENSOR, ENABLED_LEGACY_BINARY_SENSOR, - LOW_BATTERY_BINARY_SENSOR, NOTIFICATION_MOTION_BINARY_SENSOR, PROPERTY_DOOR_STATUS_BINARY_SENSOR, TAMPER_SENSOR, ) -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed @pytest.fixture @@ -34,21 +37,56 @@ def platforms() -> list[str]: return [Platform.BINARY_SENSOR] -async def test_low_battery_sensor( - hass: HomeAssistant, entity_registry: er.EntityRegistry, multisensor_6, integration +async def test_battery_sensors( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + ring_keypad: Node, + integration: MockConfigEntry, ) -> None: - """Test boolean binary sensor of type low battery.""" - state = hass.states.get(LOW_BATTERY_BINARY_SENSOR) + """Test boolean battery binary sensors.""" + entity_id = "binary_sensor.keypad_v2_low_battery_level" + state = hass.states.get(entity_id) assert state assert state.state == STATE_OFF assert state.attributes[ATTR_DEVICE_CLASS] == BinarySensorDeviceClass.BATTERY - entity_entry = entity_registry.async_get(LOW_BATTERY_BINARY_SENSOR) + entity_entry = entity_registry.async_get(entity_id) assert entity_entry assert entity_entry.entity_category is EntityCategory.DIAGNOSTIC + disabled_binary_sensor_battery_entities = ( + "binary_sensor.keypad_v2_battery_is_disconnected", + "binary_sensor.keypad_v2_fluid_is_low", + "binary_sensor.keypad_v2_overheating", + "binary_sensor.keypad_v2_rechargeable", + "binary_sensor.keypad_v2_used_as_backup", + ) + + for entity_id in disabled_binary_sensor_battery_entities: + state = hass.states.get(entity_id) + assert state is None # disabled by default + + entity_entry = entity_registry.async_get(entity_id) + + assert entity_entry + assert entity_entry.entity_category is EntityCategory.DIAGNOSTIC + assert entity_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION + + entity_registry.async_update_entity(entity_id, disabled_by=None) + + async_fire_time_changed( + hass, + dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), + ) + await hass.async_block_till_done() + + for entity_id in disabled_binary_sensor_battery_entities: + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_OFF + async def test_enabled_legacy_sensor( hass: HomeAssistant, ecolink_door_sensor, integration diff --git a/tests/components/zwave_js/test_sensor.py b/tests/components/zwave_js/test_sensor.py index c93b722334b..c3580df1f27 100644 --- a/tests/components/zwave_js/test_sensor.py +++ b/tests/components/zwave_js/test_sensor.py @@ -1,6 +1,7 @@ """Test the Z-Wave JS sensor platform.""" import copy +from datetime import timedelta import pytest from zwave_js_server.const.command_class.meter import MeterType @@ -26,6 +27,7 @@ from homeassistant.components.zwave_js.sensor import ( CONTROLLER_STATISTICS_KEY_MAP, NODE_STATISTICS_KEY_MAP, ) +from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, @@ -35,6 +37,7 @@ from homeassistant.const import ( STATE_UNKNOWN, UV_INDEX, EntityCategory, + Platform, UnitOfElectricCurrent, UnitOfElectricPotential, UnitOfEnergy, @@ -45,6 +48,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er +from homeassistant.util import dt as dt_util from .common import ( AIR_TEMPERATURE_SENSOR, @@ -57,7 +61,94 @@ from .common import ( VOLTAGE_SENSOR, ) -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed + + +@pytest.fixture +def platforms() -> list[str]: + """Fixture to specify platforms to test.""" + return [Platform.SENSOR] + + +async def test_battery_sensors( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + ring_keypad: Node, + integration: MockConfigEntry, +) -> None: + """Test numeric battery sensors.""" + entity_id = "sensor.keypad_v2_battery_level" + state = hass.states.get(entity_id) + assert state + assert state.state == "100.0" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.BATTERY + assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT + + entity_entry = entity_registry.async_get(entity_id) + assert entity_entry + assert entity_entry.entity_category is EntityCategory.DIAGNOSTIC + + disabled_sensor_battery_entities = ( + "sensor.keypad_v2_chargingstatus", + "sensor.keypad_v2_maximum_capacity", + "sensor.keypad_v2_rechargeorreplace", + "sensor.keypad_v2_temperature", + ) + + for entity_id in disabled_sensor_battery_entities: + state = hass.states.get(entity_id) + assert state is None # disabled by default + + entity_entry = entity_registry.async_get(entity_id) + + assert entity_entry + assert entity_entry.entity_category is EntityCategory.DIAGNOSTIC + assert entity_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION + + entity_registry.async_update_entity(entity_id, disabled_by=None) + + async_fire_time_changed( + hass, + dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), + ) + await hass.async_block_till_done() + + entity_id = "sensor.keypad_v2_chargingstatus" + state = hass.states.get(entity_id) + assert state + assert state.state == "Maintaining" + assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.ENUM + assert ATTR_STATE_CLASS not in state.attributes + + entity_id = "sensor.keypad_v2_maximum_capacity" + state = hass.states.get(entity_id) + assert state + assert ( + state.state == "0" + ) # This should be None/unknown but will be fixed in a future PR. + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE + assert ATTR_DEVICE_CLASS not in state.attributes + assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT + + entity_id = "sensor.keypad_v2_rechargeorreplace" + state = hass.states.get(entity_id) + assert state + assert state.state == "No" + assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.ENUM + assert ATTR_STATE_CLASS not in state.attributes + + entity_id = "sensor.keypad_v2_temperature" + state = hass.states.get(entity_id) + assert state + assert ( + state.state == "0" + ) # This should be None/unknown but will be fixed in a future PR. + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE + assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT async def test_numeric_sensor( From 458aa3cc2272868852e03b723e57fe9bdda4d37b Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Fri, 20 Jun 2025 18:39:43 +1000 Subject: [PATCH 252/266] Fix Charge Cable binary sensor in Teslemetry (#147136) --- homeassistant/components/teslemetry/binary_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/teslemetry/binary_sensor.py b/homeassistant/components/teslemetry/binary_sensor.py index a32c5fea40e..439df76c838 100644 --- a/homeassistant/components/teslemetry/binary_sensor.py +++ b/homeassistant/components/teslemetry/binary_sensor.py @@ -126,7 +126,7 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryBinarySensorEntityDescription, ...] = ( polling=True, polling_value_fn=lambda x: x != "", streaming_listener=lambda vehicle, callback: vehicle.listen_ChargingCableType( - lambda value: callback(value != "Unknown") + lambda value: callback(value is not None and value != "Unknown") ), entity_category=EntityCategory.DIAGNOSTIC, device_class=BinarySensorDeviceClass.CONNECTIVITY, From 3534396028d606ba20c8cd72ca828e48c6221993 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 19 Jun 2025 17:42:30 +0200 Subject: [PATCH 253/266] [ci] Bump cache key version (#147148) --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index af0bdc5c2df..bde1635f692 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -37,7 +37,7 @@ on: type: boolean env: - CACHE_VERSION: 2 + CACHE_VERSION: 3 UV_CACHE_VERSION: 1 MYPY_CACHE_VERSION: 1 HA_SHORT_VERSION: "2025.6" From 802fcab1c6cfeede2d67ff10e396f38c0bddfe75 Mon Sep 17 00:00:00 2001 From: hahn-th <15319212+hahn-th@users.noreply.github.com> Date: Thu, 19 Jun 2025 18:55:14 +0200 Subject: [PATCH 254/266] Bump homematicip to 2.0.6 (#147151) --- homeassistant/components/homematicip_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index 163f3c402dc..d5af2859873 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.5"] + "requirements": ["homematicip==2.0.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index 6d09a5f9037..11bb82d6557 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1170,7 +1170,7 @@ home-assistant-frontend==20250531.3 home-assistant-intents==2025.6.10 # homeassistant.components.homematicip_cloud -homematicip==2.0.5 +homematicip==2.0.6 # homeassistant.components.horizon horimote==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6c65be62ef2..daf7193ee3a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1016,7 +1016,7 @@ home-assistant-frontend==20250531.3 home-assistant-intents==2025.6.10 # homeassistant.components.homematicip_cloud -homematicip==2.0.5 +homematicip==2.0.6 # homeassistant.components.remember_the_milk httplib2==0.20.4 From 7cc6e28916862562999621d34926d3eead8bccd9 Mon Sep 17 00:00:00 2001 From: Hessel Date: Mon, 23 Jun 2025 14:10:50 +0200 Subject: [PATCH 255/266] Wallbox fix too many requests by API (#147197) --- homeassistant/components/wallbox/const.py | 2 +- .../components/wallbox/coordinator.py | 177 ++++++--- homeassistant/components/wallbox/lock.py | 4 +- homeassistant/components/wallbox/number.py | 4 +- homeassistant/components/wallbox/sensor.py | 6 - homeassistant/components/wallbox/strings.json | 3 + tests/components/wallbox/__init__.py | 21 + tests/components/wallbox/test_config_flow.py | 142 +++---- tests/components/wallbox/test_init.py | 125 ++++-- tests/components/wallbox/test_lock.py | 124 ++++-- tests/components/wallbox/test_number.py | 370 +++++++++++------- tests/components/wallbox/test_select.py | 37 +- tests/components/wallbox/test_switch.py | 131 +++---- 13 files changed, 692 insertions(+), 454 deletions(-) diff --git a/homeassistant/components/wallbox/const.py b/homeassistant/components/wallbox/const.py index 5aa659a0527..34d17e52275 100644 --- a/homeassistant/components/wallbox/const.py +++ b/homeassistant/components/wallbox/const.py @@ -3,7 +3,7 @@ from enum import StrEnum DOMAIN = "wallbox" -UPDATE_INTERVAL = 30 +UPDATE_INTERVAL = 60 BIDIRECTIONAL_MODEL_PREFIXES = ["QS"] diff --git a/homeassistant/components/wallbox/coordinator.py b/homeassistant/components/wallbox/coordinator.py index 8276ee14eaf..598bfa7429a 100644 --- a/homeassistant/components/wallbox/coordinator.py +++ b/homeassistant/components/wallbox/coordinator.py @@ -90,7 +90,9 @@ def _require_authentication[_WallboxCoordinatorT: WallboxCoordinator, **_P]( except requests.exceptions.HTTPError as wallbox_connection_error: if wallbox_connection_error.response.status_code == HTTPStatus.FORBIDDEN: raise ConfigEntryAuthFailed from wallbox_connection_error - raise ConnectionError from wallbox_connection_error + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="api_failed" + ) from wallbox_connection_error return require_authentication @@ -137,56 +139,65 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]): @_require_authentication def _get_data(self) -> dict[str, Any]: """Get new sensor data for Wallbox component.""" - data: dict[str, Any] = self._wallbox.getChargerStatus(self._station) - data[CHARGER_MAX_CHARGING_CURRENT_KEY] = data[CHARGER_DATA_KEY][ - CHARGER_MAX_CHARGING_CURRENT_KEY - ] - data[CHARGER_LOCKED_UNLOCKED_KEY] = data[CHARGER_DATA_KEY][ - CHARGER_LOCKED_UNLOCKED_KEY - ] - data[CHARGER_ENERGY_PRICE_KEY] = data[CHARGER_DATA_KEY][ - CHARGER_ENERGY_PRICE_KEY - ] - # Only show max_icp_current if power_boost is available in the wallbox unit: - if ( - data[CHARGER_DATA_KEY].get(CHARGER_MAX_ICP_CURRENT_KEY, 0) > 0 - and CHARGER_POWER_BOOST_KEY - in data[CHARGER_DATA_KEY][CHARGER_PLAN_KEY][CHARGER_FEATURES_KEY] - ): - data[CHARGER_MAX_ICP_CURRENT_KEY] = data[CHARGER_DATA_KEY][ - CHARGER_MAX_ICP_CURRENT_KEY + try: + data: dict[str, Any] = self._wallbox.getChargerStatus(self._station) + data[CHARGER_MAX_CHARGING_CURRENT_KEY] = data[CHARGER_DATA_KEY][ + CHARGER_MAX_CHARGING_CURRENT_KEY ] + data[CHARGER_LOCKED_UNLOCKED_KEY] = data[CHARGER_DATA_KEY][ + CHARGER_LOCKED_UNLOCKED_KEY + ] + data[CHARGER_ENERGY_PRICE_KEY] = data[CHARGER_DATA_KEY][ + CHARGER_ENERGY_PRICE_KEY + ] + # Only show max_icp_current if power_boost is available in the wallbox unit: + if ( + data[CHARGER_DATA_KEY].get(CHARGER_MAX_ICP_CURRENT_KEY, 0) > 0 + and CHARGER_POWER_BOOST_KEY + in data[CHARGER_DATA_KEY][CHARGER_PLAN_KEY][CHARGER_FEATURES_KEY] + ): + data[CHARGER_MAX_ICP_CURRENT_KEY] = data[CHARGER_DATA_KEY][ + CHARGER_MAX_ICP_CURRENT_KEY + ] - data[CHARGER_CURRENCY_KEY] = ( - f"{data[CHARGER_DATA_KEY][CHARGER_CURRENCY_KEY][CODE_KEY]}/kWh" - ) + data[CHARGER_CURRENCY_KEY] = ( + f"{data[CHARGER_DATA_KEY][CHARGER_CURRENCY_KEY][CODE_KEY]}/kWh" + ) - data[CHARGER_STATUS_DESCRIPTION_KEY] = CHARGER_STATUS.get( - data[CHARGER_STATUS_ID_KEY], ChargerStatus.UNKNOWN - ) + data[CHARGER_STATUS_DESCRIPTION_KEY] = CHARGER_STATUS.get( + data[CHARGER_STATUS_ID_KEY], ChargerStatus.UNKNOWN + ) - # Set current solar charging mode - eco_smart_enabled = ( - data[CHARGER_DATA_KEY] - .get(CHARGER_ECO_SMART_KEY, {}) - .get(CHARGER_ECO_SMART_STATUS_KEY) - ) + # Set current solar charging mode + eco_smart_enabled = ( + data[CHARGER_DATA_KEY] + .get(CHARGER_ECO_SMART_KEY, {}) + .get(CHARGER_ECO_SMART_STATUS_KEY) + ) - eco_smart_mode = ( - data[CHARGER_DATA_KEY] - .get(CHARGER_ECO_SMART_KEY, {}) - .get(CHARGER_ECO_SMART_MODE_KEY) - ) - if eco_smart_mode is None: - data[CHARGER_ECO_SMART_KEY] = EcoSmartMode.DISABLED - elif eco_smart_enabled is False: - data[CHARGER_ECO_SMART_KEY] = EcoSmartMode.OFF - elif eco_smart_mode == 0: - data[CHARGER_ECO_SMART_KEY] = EcoSmartMode.ECO_MODE - elif eco_smart_mode == 1: - data[CHARGER_ECO_SMART_KEY] = EcoSmartMode.FULL_SOLAR + eco_smart_mode = ( + data[CHARGER_DATA_KEY] + .get(CHARGER_ECO_SMART_KEY, {}) + .get(CHARGER_ECO_SMART_MODE_KEY) + ) + if eco_smart_mode is None: + data[CHARGER_ECO_SMART_KEY] = EcoSmartMode.DISABLED + elif eco_smart_enabled is False: + data[CHARGER_ECO_SMART_KEY] = EcoSmartMode.OFF + elif eco_smart_mode == 0: + data[CHARGER_ECO_SMART_KEY] = EcoSmartMode.ECO_MODE + elif eco_smart_mode == 1: + data[CHARGER_ECO_SMART_KEY] = EcoSmartMode.FULL_SOLAR - return data + return data # noqa: TRY300 + except requests.exceptions.HTTPError as wallbox_connection_error: + if wallbox_connection_error.response.status_code == 429: + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="too_many_requests" + ) from wallbox_connection_error + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="api_failed" + ) from wallbox_connection_error async def _async_update_data(self) -> dict[str, Any]: """Get new sensor data for Wallbox component.""" @@ -200,7 +211,13 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]): except requests.exceptions.HTTPError as wallbox_connection_error: if wallbox_connection_error.response.status_code == 403: raise InvalidAuth from wallbox_connection_error - raise + if wallbox_connection_error.response.status_code == 429: + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="too_many_requests" + ) from wallbox_connection_error + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="api_failed" + ) from wallbox_connection_error async def async_set_charging_current(self, charging_current: float) -> None: """Set maximum charging current for Wallbox.""" @@ -217,7 +234,13 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]): except requests.exceptions.HTTPError as wallbox_connection_error: if wallbox_connection_error.response.status_code == 403: raise InvalidAuth from wallbox_connection_error - raise + if wallbox_connection_error.response.status_code == 429: + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="too_many_requests" + ) from wallbox_connection_error + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="api_failed" + ) from wallbox_connection_error async def async_set_icp_current(self, icp_current: float) -> None: """Set maximum icp current for Wallbox.""" @@ -227,8 +250,16 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]): @_require_authentication def _set_energy_cost(self, energy_cost: float) -> None: """Set energy cost for Wallbox.""" - - self._wallbox.setEnergyCost(self._station, energy_cost) + try: + self._wallbox.setEnergyCost(self._station, energy_cost) + except requests.exceptions.HTTPError as wallbox_connection_error: + if wallbox_connection_error.response.status_code == 429: + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="too_many_requests" + ) from wallbox_connection_error + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="api_failed" + ) from wallbox_connection_error async def async_set_energy_cost(self, energy_cost: float) -> None: """Set energy cost for Wallbox.""" @@ -246,7 +277,13 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]): except requests.exceptions.HTTPError as wallbox_connection_error: if wallbox_connection_error.response.status_code == 403: raise InvalidAuth from wallbox_connection_error - raise + if wallbox_connection_error.response.status_code == 429: + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="too_many_requests" + ) from wallbox_connection_error + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="api_failed" + ) from wallbox_connection_error async def async_set_lock_unlock(self, lock: bool) -> None: """Set wallbox to locked or unlocked.""" @@ -256,11 +293,19 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]): @_require_authentication def _pause_charger(self, pause: bool) -> None: """Set wallbox to pause or resume.""" - - if pause: - self._wallbox.pauseChargingSession(self._station) - else: - self._wallbox.resumeChargingSession(self._station) + try: + if pause: + self._wallbox.pauseChargingSession(self._station) + else: + self._wallbox.resumeChargingSession(self._station) + except requests.exceptions.HTTPError as wallbox_connection_error: + if wallbox_connection_error.response.status_code == 429: + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="too_many_requests" + ) from wallbox_connection_error + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="api_failed" + ) from wallbox_connection_error async def async_pause_charger(self, pause: bool) -> None: """Set wallbox to pause or resume.""" @@ -270,13 +315,21 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]): @_require_authentication def _set_eco_smart(self, option: str) -> None: """Set wallbox solar charging mode.""" - - if option == EcoSmartMode.ECO_MODE: - self._wallbox.enableEcoSmart(self._station, 0) - elif option == EcoSmartMode.FULL_SOLAR: - self._wallbox.enableEcoSmart(self._station, 1) - else: - self._wallbox.disableEcoSmart(self._station) + try: + if option == EcoSmartMode.ECO_MODE: + self._wallbox.enableEcoSmart(self._station, 0) + elif option == EcoSmartMode.FULL_SOLAR: + self._wallbox.enableEcoSmart(self._station, 1) + else: + self._wallbox.disableEcoSmart(self._station) + except requests.exceptions.HTTPError as wallbox_connection_error: + if wallbox_connection_error.response.status_code == 429: + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="too_many_requests" + ) from wallbox_connection_error + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="api_failed" + ) from wallbox_connection_error async def async_set_eco_smart(self, option: str) -> None: """Set wallbox solar charging mode.""" diff --git a/homeassistant/components/wallbox/lock.py b/homeassistant/components/wallbox/lock.py index ef35734ed7e..7acc56f67f2 100644 --- a/homeassistant/components/wallbox/lock.py +++ b/homeassistant/components/wallbox/lock.py @@ -7,7 +7,7 @@ from typing import Any from homeassistant.components.lock import LockEntity, LockEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.exceptions import PlatformNotReady +from homeassistant.exceptions import HomeAssistantError, PlatformNotReady from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .const import ( @@ -41,7 +41,7 @@ async def async_setup_entry( ) except InvalidAuth: return - except ConnectionError as exc: + except HomeAssistantError as exc: raise PlatformNotReady from exc async_add_entities( diff --git a/homeassistant/components/wallbox/number.py b/homeassistant/components/wallbox/number.py index a5880f6e0f7..80773478582 100644 --- a/homeassistant/components/wallbox/number.py +++ b/homeassistant/components/wallbox/number.py @@ -12,7 +12,7 @@ from typing import cast from homeassistant.components.number import NumberEntity, NumberEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.exceptions import PlatformNotReady +from homeassistant.exceptions import HomeAssistantError, PlatformNotReady from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .const import ( @@ -93,7 +93,7 @@ async def async_setup_entry( ) except InvalidAuth: return - except ConnectionError as exc: + except HomeAssistantError as exc: raise PlatformNotReady from exc async_add_entities( diff --git a/homeassistant/components/wallbox/sensor.py b/homeassistant/components/wallbox/sensor.py index 4b0ec8175e3..e19fc2b936a 100644 --- a/homeassistant/components/wallbox/sensor.py +++ b/homeassistant/components/wallbox/sensor.py @@ -3,7 +3,6 @@ from __future__ import annotations from dataclasses import dataclass -import logging from typing import cast from homeassistant.components.sensor import ( @@ -49,11 +48,6 @@ from .const import ( from .coordinator import WallboxCoordinator from .entity import WallboxEntity -CHARGER_STATION = "station" -UPDATE_INTERVAL = 30 - -_LOGGER = logging.getLogger(__name__) - @dataclass(frozen=True) class WallboxSensorEntityDescription(SensorEntityDescription): diff --git a/homeassistant/components/wallbox/strings.json b/homeassistant/components/wallbox/strings.json index 68602a960c2..ee98a4855e3 100644 --- a/homeassistant/components/wallbox/strings.json +++ b/homeassistant/components/wallbox/strings.json @@ -112,6 +112,9 @@ "exceptions": { "api_failed": { "message": "Error communicating with Wallbox API" + }, + "too_many_requests": { + "message": "Error communicating with Wallbox API, too many requests" } } } diff --git a/tests/components/wallbox/__init__.py b/tests/components/wallbox/__init__.py index 83e39d2f602..37e7d5059f0 100644 --- a/tests/components/wallbox/__init__.py +++ b/tests/components/wallbox/__init__.py @@ -162,6 +162,9 @@ test_response_no_power_boost = { http_404_error = requests.exceptions.HTTPError() http_404_error.response = requests.Response() http_404_error.response.status_code = HTTPStatus.NOT_FOUND +http_429_error = requests.exceptions.HTTPError() +http_429_error.response = requests.Response() +http_429_error.response.status_code = HTTPStatus.TOO_MANY_REQUESTS authorisation_response = { "data": { @@ -192,6 +195,24 @@ authorisation_response_unauthorised = { } } +invalid_reauth_response = { + "jwt": "fakekeyhere", + "refresh_token": "refresh_fakekeyhere", + "user_id": 12345, + "ttl": 145656758, + "refresh_token_ttl": 145756758, + "error": False, + "status": 200, +} + +http_403_error = requests.exceptions.HTTPError() +http_403_error.response = requests.Response() +http_403_error.response.status_code = HTTPStatus.FORBIDDEN + +http_404_error = requests.exceptions.HTTPError() +http_404_error.response = requests.Response() +http_404_error.response.status_code = HTTPStatus.NOT_FOUND + async def setup_integration(hass: HomeAssistant, entry: MockConfigEntry) -> None: """Test wallbox sensor class setup.""" diff --git a/tests/components/wallbox/test_config_flow.py b/tests/components/wallbox/test_config_flow.py index 467e20c51c1..bdfb4cad18d 100644 --- a/tests/components/wallbox/test_config_flow.py +++ b/tests/components/wallbox/test_config_flow.py @@ -1,9 +1,6 @@ """Test the Wallbox config flow.""" -from http import HTTPStatus -import json - -import requests_mock +from unittest.mock import Mock, patch from homeassistant import config_entries from homeassistant.components.wallbox import config_flow @@ -24,23 +21,21 @@ from homeassistant.data_entry_flow import FlowResultType from . import ( authorisation_response, authorisation_response_unauthorised, + http_403_error, + http_404_error, setup_integration, ) from tests.common import MockConfigEntry -test_response = json.loads( - json.dumps( - { - CHARGER_CHARGING_POWER_KEY: 0, - CHARGER_MAX_AVAILABLE_POWER_KEY: "xx", - CHARGER_CHARGING_SPEED_KEY: 0, - CHARGER_ADDED_RANGE_KEY: "xx", - CHARGER_ADDED_ENERGY_KEY: "44.697", - CHARGER_DATA_KEY: {CHARGER_MAX_CHARGING_CURRENT_KEY: 24}, - } - ) -) +test_response = { + CHARGER_CHARGING_POWER_KEY: 0, + CHARGER_MAX_AVAILABLE_POWER_KEY: "xx", + CHARGER_CHARGING_SPEED_KEY: 0, + CHARGER_ADDED_RANGE_KEY: "xx", + CHARGER_ADDED_ENERGY_KEY: "44.697", + CHARGER_DATA_KEY: {CHARGER_MAX_CHARGING_CURRENT_KEY: 24}, +} async def test_show_set_form(hass: HomeAssistant) -> None: @@ -59,17 +54,16 @@ async def test_form_cannot_authenticate(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with requests_mock.Mocker() as mock_request: - mock_request.get( - "https://user-api.wall-box.com/users/signin", - json=authorisation_response, - status_code=HTTPStatus.FORBIDDEN, - ) - mock_request.get( - "https://api.wall-box.com/chargers/status/12345", - json=test_response, - status_code=HTTPStatus.FORBIDDEN, - ) + with ( + patch( + "homeassistant.components.wallbox.Wallbox.authenticate", + new=Mock(side_effect=http_403_error), + ), + patch( + "homeassistant.components.wallbox.Wallbox.pauseChargingSession", + new=Mock(side_effect=http_403_error), + ), + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -89,17 +83,16 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with requests_mock.Mocker() as mock_request: - mock_request.get( - "https://user-api.wall-box.com/users/signin", - json=authorisation_response_unauthorised, - status_code=HTTPStatus.NOT_FOUND, - ) - mock_request.get( - "https://api.wall-box.com/chargers/status/12345", - json=test_response, - status_code=HTTPStatus.NOT_FOUND, - ) + with ( + patch( + "homeassistant.components.wallbox.Wallbox.authenticate", + new=Mock(side_effect=http_404_error), + ), + patch( + "homeassistant.components.wallbox.Wallbox.pauseChargingSession", + new=Mock(side_effect=http_404_error), + ), + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -119,17 +112,16 @@ async def test_form_validate_input(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with requests_mock.Mocker() as mock_request: - mock_request.get( - "https://user-api.wall-box.com/users/signin", - json=authorisation_response, - status_code=HTTPStatus.OK, - ) - mock_request.get( - "https://api.wall-box.com/chargers/status/12345", - json=test_response, - status_code=HTTPStatus.OK, - ) + with ( + patch( + "homeassistant.components.wallbox.Wallbox.authenticate", + new=Mock(return_value=authorisation_response), + ), + patch( + "homeassistant.components.wallbox.Wallbox.getChargerStatus", + new=Mock(return_value=test_response), + ), + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -148,18 +140,16 @@ async def test_form_reauth(hass: HomeAssistant, entry: MockConfigEntry) -> None: await setup_integration(hass, entry) assert entry.state is ConfigEntryState.LOADED - with requests_mock.Mocker() as mock_request: - mock_request.get( - "https://user-api.wall-box.com/users/signin", - json=authorisation_response, - status_code=200, - ) - mock_request.get( - "https://api.wall-box.com/chargers/status/12345", - json=test_response, - status_code=200, - ) - + with ( + patch( + "homeassistant.components.wallbox.Wallbox.authenticate", + new=Mock(return_value=authorisation_response_unauthorised), + ), + patch( + "homeassistant.components.wallbox.Wallbox.getChargerStatus", + new=Mock(return_value=test_response), + ), + ): result = await entry.start_reauth_flow(hass) result2 = await hass.config_entries.flow.async_configure( @@ -183,26 +173,16 @@ async def test_form_reauth_invalid(hass: HomeAssistant, entry: MockConfigEntry) await setup_integration(hass, entry) assert entry.state is ConfigEntryState.LOADED - with requests_mock.Mocker() as mock_request: - mock_request.get( - "https://user-api.wall-box.com/users/signin", - json={ - "jwt": "fakekeyhere", - "refresh_token": "refresh_fakekeyhere", - "user_id": 12345, - "ttl": 145656758, - "refresh_token_ttl": 145756758, - "error": False, - "status": 200, - }, - status_code=200, - ) - mock_request.get( - "https://api.wall-box.com/chargers/status/12345", - json=test_response, - status_code=200, - ) - + with ( + patch( + "homeassistant.components.wallbox.Wallbox.authenticate", + new=Mock(return_value=authorisation_response_unauthorised), + ), + patch( + "homeassistant.components.wallbox.Wallbox.getChargerStatus", + new=Mock(return_value=test_response), + ), + ): result = await entry.start_reauth_flow(hass) result2 = await hass.config_entries.flow.async_configure( diff --git a/tests/components/wallbox/test_init.py b/tests/components/wallbox/test_init.py index 6d6a5cd1417..5048385aaf6 100644 --- a/tests/components/wallbox/test_init.py +++ b/tests/components/wallbox/test_init.py @@ -1,16 +1,15 @@ """Test Wallbox Init Component.""" -import requests_mock +from unittest.mock import Mock, patch -from homeassistant.components.wallbox.const import ( - CHARGER_MAX_CHARGING_CURRENT_KEY, - DOMAIN, -) +from homeassistant.components.wallbox.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from . import ( authorisation_response, + http_403_error, + http_429_error, setup_integration, setup_integration_connection_error, setup_integration_no_eco_mode, @@ -53,18 +52,16 @@ async def test_wallbox_refresh_failed_connection_error_auth( await setup_integration(hass, entry) assert entry.state is ConfigEntryState.LOADED - with requests_mock.Mocker() as mock_request: - mock_request.get( - "https://user-api.wall-box.com/users/signin", - json=authorisation_response, - status_code=404, - ) - mock_request.get( - "https://api.wall-box.com/chargers/status/12345", - json=test_response, - status_code=200, - ) - + with ( + patch( + "homeassistant.components.wallbox.Wallbox.authenticate", + new=Mock(side_effect=http_429_error), + ), + patch( + "homeassistant.components.wallbox.Wallbox.pauseChargingSession", + new=Mock(return_value=test_response), + ), + ): wallbox = hass.data[DOMAIN][entry.entry_id] await wallbox.async_refresh() @@ -81,18 +78,68 @@ async def test_wallbox_refresh_failed_invalid_auth( await setup_integration(hass, entry) assert entry.state is ConfigEntryState.LOADED - with requests_mock.Mocker() as mock_request: - mock_request.get( - "https://user-api.wall-box.com/users/signin", - json=authorisation_response, - status_code=403, - ) - mock_request.put( - "https://api.wall-box.com/v2/charger/12345", - json={CHARGER_MAX_CHARGING_CURRENT_KEY: 20}, - status_code=403, - ) + with ( + patch( + "homeassistant.components.wallbox.Wallbox.authenticate", + new=Mock(side_effect=http_403_error), + ), + patch( + "homeassistant.components.wallbox.Wallbox.pauseChargingSession", + new=Mock(side_effect=http_403_error), + ), + ): + wallbox = hass.data[DOMAIN][entry.entry_id] + await wallbox.async_refresh() + + assert await hass.config_entries.async_unload(entry.entry_id) + assert entry.state is ConfigEntryState.NOT_LOADED + + +async def test_wallbox_refresh_failed_http_error( + hass: HomeAssistant, entry: MockConfigEntry +) -> None: + """Test Wallbox setup with authentication error.""" + + await setup_integration(hass, entry) + assert entry.state is ConfigEntryState.LOADED + + with ( + patch( + "homeassistant.components.wallbox.Wallbox.authenticate", + new=Mock(return_value=authorisation_response), + ), + patch( + "homeassistant.components.wallbox.Wallbox.getChargerStatus", + new=Mock(side_effect=http_403_error), + ), + ): + wallbox = hass.data[DOMAIN][entry.entry_id] + + await wallbox.async_refresh() + + assert await hass.config_entries.async_unload(entry.entry_id) + assert entry.state is ConfigEntryState.NOT_LOADED + + +async def test_wallbox_refresh_failed_too_many_requests( + hass: HomeAssistant, entry: MockConfigEntry +) -> None: + """Test Wallbox setup with authentication error.""" + + await setup_integration(hass, entry) + assert entry.state is ConfigEntryState.LOADED + + with ( + patch( + "homeassistant.components.wallbox.Wallbox.authenticate", + new=Mock(return_value=authorisation_response), + ), + patch( + "homeassistant.components.wallbox.Wallbox.getChargerStatus", + new=Mock(side_effect=http_429_error), + ), + ): wallbox = hass.data[DOMAIN][entry.entry_id] await wallbox.async_refresh() @@ -109,18 +156,16 @@ async def test_wallbox_refresh_failed_connection_error( await setup_integration(hass, entry) assert entry.state is ConfigEntryState.LOADED - with requests_mock.Mocker() as mock_request: - mock_request.get( - "https://user-api.wall-box.com/users/signin", - json=authorisation_response, - status_code=200, - ) - mock_request.get( - "https://api.wall-box.com/chargers/status/12345", - json=test_response, - status_code=403, - ) - + with ( + patch( + "homeassistant.components.wallbox.Wallbox.authenticate", + new=Mock(return_value=authorisation_response), + ), + patch( + "homeassistant.components.wallbox.Wallbox.pauseChargingSession", + new=Mock(side_effect=http_403_error), + ), + ): wallbox = hass.data[DOMAIN][entry.entry_id] await wallbox.async_refresh() diff --git a/tests/components/wallbox/test_lock.py b/tests/components/wallbox/test_lock.py index 1d48e53b515..5842d708f11 100644 --- a/tests/components/wallbox/test_lock.py +++ b/tests/components/wallbox/test_lock.py @@ -1,15 +1,18 @@ """Test Wallbox Lock component.""" +from unittest.mock import Mock, patch + import pytest -import requests_mock from homeassistant.components.lock import SERVICE_LOCK, SERVICE_UNLOCK from homeassistant.components.wallbox.const import CHARGER_LOCKED_UNLOCKED_KEY from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from . import ( authorisation_response, + http_429_error, setup_integration, setup_integration_platform_not_ready, setup_integration_read_only, @@ -28,18 +31,20 @@ async def test_wallbox_lock_class(hass: HomeAssistant, entry: MockConfigEntry) - assert state assert state.state == "unlocked" - with requests_mock.Mocker() as mock_request: - mock_request.get( - "https://user-api.wall-box.com/users/signin", - json=authorisation_response, - status_code=200, - ) - mock_request.put( - "https://api.wall-box.com/v2/charger/12345", - json={CHARGER_LOCKED_UNLOCKED_KEY: False}, - status_code=200, - ) - + with ( + patch( + "homeassistant.components.wallbox.Wallbox.authenticate", + new=Mock(return_value=authorisation_response), + ), + patch( + "homeassistant.components.wallbox.Wallbox.lockCharger", + new=Mock(return_value={CHARGER_LOCKED_UNLOCKED_KEY: False}), + ), + patch( + "homeassistant.components.wallbox.Wallbox.unlockCharger", + new=Mock(return_value={CHARGER_LOCKED_UNLOCKED_KEY: False}), + ), + ): await hass.services.async_call( "lock", SERVICE_LOCK, @@ -66,36 +71,73 @@ async def test_wallbox_lock_class_connection_error( await setup_integration(hass, entry) - with requests_mock.Mocker() as mock_request: - mock_request.get( - "https://user-api.wall-box.com/users/signin", - json=authorisation_response, - status_code=200, - ) - mock_request.put( - "https://api.wall-box.com/v2/charger/12345", - json={CHARGER_LOCKED_UNLOCKED_KEY: False}, - status_code=404, + with ( + patch( + "homeassistant.components.wallbox.Wallbox.authenticate", + new=Mock(return_value=authorisation_response), + ), + patch( + "homeassistant.components.wallbox.Wallbox.lockCharger", + new=Mock(side_effect=ConnectionError), + ), + pytest.raises(ConnectionError), + ): + await hass.services.async_call( + "lock", + SERVICE_LOCK, + { + ATTR_ENTITY_ID: MOCK_LOCK_ENTITY_ID, + }, + blocking=True, ) - with pytest.raises(ConnectionError): - await hass.services.async_call( - "lock", - SERVICE_LOCK, - { - ATTR_ENTITY_ID: MOCK_LOCK_ENTITY_ID, - }, - blocking=True, - ) - with pytest.raises(ConnectionError): - await hass.services.async_call( - "lock", - SERVICE_UNLOCK, - { - ATTR_ENTITY_ID: MOCK_LOCK_ENTITY_ID, - }, - blocking=True, - ) + with ( + patch( + "homeassistant.components.wallbox.Wallbox.authenticate", + new=Mock(return_value=authorisation_response), + ), + patch( + "homeassistant.components.wallbox.Wallbox.lockCharger", + new=Mock(side_effect=ConnectionError), + ), + patch( + "homeassistant.components.wallbox.Wallbox.unlockCharger", + new=Mock(side_effect=ConnectionError), + ), + pytest.raises(ConnectionError), + ): + await hass.services.async_call( + "lock", + SERVICE_UNLOCK, + { + ATTR_ENTITY_ID: MOCK_LOCK_ENTITY_ID, + }, + blocking=True, + ) + + with ( + patch( + "homeassistant.components.wallbox.Wallbox.authenticate", + new=Mock(return_value=authorisation_response), + ), + patch( + "homeassistant.components.wallbox.Wallbox.lockCharger", + new=Mock(side_effect=http_429_error), + ), + patch( + "homeassistant.components.wallbox.Wallbox.unlockCharger", + new=Mock(side_effect=http_429_error), + ), + pytest.raises(HomeAssistantError), + ): + await hass.services.async_call( + "lock", + SERVICE_UNLOCK, + { + ATTR_ENTITY_ID: MOCK_LOCK_ENTITY_ID, + }, + blocking=True, + ) async def test_wallbox_lock_class_authentication_error( diff --git a/tests/components/wallbox/test_number.py b/tests/components/wallbox/test_number.py index c319668c161..c603ae24106 100644 --- a/tests/components/wallbox/test_number.py +++ b/tests/components/wallbox/test_number.py @@ -1,22 +1,26 @@ """Test Wallbox Switch component.""" +from unittest.mock import Mock, patch + import pytest -import requests_mock from homeassistant.components.input_number import ATTR_VALUE, SERVICE_SET_VALUE from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN -from homeassistant.components.wallbox import InvalidAuth from homeassistant.components.wallbox.const import ( CHARGER_ENERGY_PRICE_KEY, CHARGER_MAX_CHARGING_CURRENT_KEY, CHARGER_MAX_ICP_CURRENT_KEY, ) +from homeassistant.components.wallbox.coordinator import InvalidAuth from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.exceptions import HomeAssistantError from . import ( authorisation_response, + http_403_error, + http_404_error, + http_429_error, setup_integration, setup_integration_bidir, setup_integration_platform_not_ready, @@ -29,6 +33,14 @@ from .const import ( from tests.common import MockConfigEntry +mock_wallbox = Mock() +mock_wallbox.authenticate = Mock(return_value=authorisation_response) +mock_wallbox.setEnergyCost = Mock(return_value={CHARGER_ENERGY_PRICE_KEY: 1.1}) +mock_wallbox.setMaxChargingCurrent = Mock( + return_value={CHARGER_MAX_CHARGING_CURRENT_KEY: 20} +) +mock_wallbox.setIcpMaxCurrent = Mock(return_value={CHARGER_MAX_ICP_CURRENT_KEY: 10}) + async def test_wallbox_number_class( hass: HomeAssistant, entry: MockConfigEntry @@ -37,17 +49,16 @@ async def test_wallbox_number_class( await setup_integration(hass, entry) - with requests_mock.Mocker() as mock_request: - mock_request.get( - "https://user-api.wall-box.com/users/signin", - json=authorisation_response, - status_code=200, - ) - mock_request.put( - "https://api.wall-box.com/v2/charger/12345", - json={CHARGER_MAX_CHARGING_CURRENT_KEY: 20}, - status_code=200, - ) + with ( + patch( + "homeassistant.components.wallbox.Wallbox.authenticate", + new=Mock(return_value=authorisation_response), + ), + patch( + "homeassistant.components.wallbox.Wallbox.setMaxChargingCurrent", + new=Mock(return_value={CHARGER_MAX_CHARGING_CURRENT_KEY: 20}), + ), + ): state = hass.states.get(MOCK_NUMBER_ENTITY_ID) assert state.attributes["min"] == 6 assert state.attributes["max"] == 25 @@ -82,19 +93,16 @@ async def test_wallbox_number_energy_class( await setup_integration(hass, entry) - with requests_mock.Mocker() as mock_request: - mock_request.get( - "https://user-api.wall-box.com/users/signin", - json=authorisation_response, - status_code=200, - ) - - mock_request.post( - "https://api.wall-box.com/chargers/config/12345", - json={CHARGER_ENERGY_PRICE_KEY: 1.1}, - status_code=200, - ) - + with ( + patch( + "homeassistant.components.wallbox.Wallbox.authenticate", + new=Mock(return_value=authorisation_response), + ), + patch( + "homeassistant.components.wallbox.Wallbox.setEnergyCost", + new=Mock(return_value={CHARGER_ENERGY_PRICE_KEY: 1.1}), + ), + ): await hass.services.async_call( "number", SERVICE_SET_VALUE, @@ -113,59 +121,113 @@ async def test_wallbox_number_class_connection_error( await setup_integration(hass, entry) - with requests_mock.Mocker() as mock_request: - mock_request.get( - "https://user-api.wall-box.com/users/signin", - json=authorisation_response, - status_code=200, - ) - mock_request.put( - "https://api.wall-box.com/v2/charger/12345", - json={CHARGER_MAX_CHARGING_CURRENT_KEY: 20}, - status_code=404, + with ( + patch( + "homeassistant.components.wallbox.Wallbox.authenticate", + new=Mock(return_value=authorisation_response), + ), + patch( + "homeassistant.components.wallbox.Wallbox.setMaxChargingCurrent", + new=Mock(side_effect=http_404_error), + ), + pytest.raises(HomeAssistantError), + ): + await hass.services.async_call( + "number", + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ID, + ATTR_VALUE: 20, + }, + blocking=True, ) - with pytest.raises(ConnectionError): - await hass.services.async_call( - "number", - SERVICE_SET_VALUE, - { - ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ID, - ATTR_VALUE: 20, - }, - blocking=True, - ) - -async def test_wallbox_number_class_energy_price_connection_error( +async def test_wallbox_number_class_too_many_requests( hass: HomeAssistant, entry: MockConfigEntry ) -> None: """Test wallbox sensor class.""" await setup_integration(hass, entry) - with requests_mock.Mocker() as mock_request: - mock_request.get( - "https://user-api.wall-box.com/users/signin", - json=authorisation_response, - status_code=200, - ) - mock_request.post( - "https://api.wall-box.com/chargers/config/12345", - json={CHARGER_ENERGY_PRICE_KEY: 1.1}, - status_code=404, + with ( + patch( + "homeassistant.components.wallbox.Wallbox.authenticate", + new=Mock(return_value=authorisation_response), + ), + patch( + "homeassistant.components.wallbox.Wallbox.setMaxChargingCurrent", + new=Mock(side_effect=http_429_error), + ), + pytest.raises(HomeAssistantError), + ): + await hass.services.async_call( + "number", + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ID, + ATTR_VALUE: 20, + }, + blocking=True, ) - with pytest.raises(ConnectionError): - await hass.services.async_call( - "number", - SERVICE_SET_VALUE, - { - ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ENERGY_PRICE_ID, - ATTR_VALUE: 1.1, - }, - blocking=True, - ) + +async def test_wallbox_number_class_energy_price_update_failed( + hass: HomeAssistant, entry: MockConfigEntry +) -> None: + """Test wallbox sensor class.""" + + await setup_integration(hass, entry) + + with ( + patch( + "homeassistant.components.wallbox.Wallbox.authenticate", + new=Mock(return_value=authorisation_response), + ), + patch( + "homeassistant.components.wallbox.Wallbox.setEnergyCost", + new=Mock(side_effect=http_429_error), + ), + pytest.raises(HomeAssistantError), + ): + await hass.services.async_call( + "number", + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ENERGY_PRICE_ID, + ATTR_VALUE: 1.1, + }, + blocking=True, + ) + + +async def test_wallbox_number_class_energy_price_update_connection_error( + hass: HomeAssistant, entry: MockConfigEntry +) -> None: + """Test wallbox sensor class.""" + + await setup_integration(hass, entry) + + with ( + patch( + "homeassistant.components.wallbox.Wallbox.authenticate", + new=Mock(return_value=authorisation_response), + ), + patch( + "homeassistant.components.wallbox.Wallbox.setEnergyCost", + new=Mock(side_effect=http_404_error), + ), + pytest.raises(HomeAssistantError), + ): + await hass.services.async_call( + "number", + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ENERGY_PRICE_ID, + ATTR_VALUE: 1.1, + }, + blocking=True, + ) async def test_wallbox_number_class_energy_price_auth_error( @@ -175,28 +237,26 @@ async def test_wallbox_number_class_energy_price_auth_error( await setup_integration(hass, entry) - with requests_mock.Mocker() as mock_request: - mock_request.get( - "https://user-api.wall-box.com/users/signin", - json=authorisation_response, - status_code=200, + with ( + patch( + "homeassistant.components.wallbox.Wallbox.authenticate", + new=Mock(return_value=authorisation_response), + ), + patch( + "homeassistant.components.wallbox.Wallbox.setEnergyCost", + new=Mock(side_effect=http_429_error), + ), + pytest.raises(HomeAssistantError), + ): + await hass.services.async_call( + "number", + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ENERGY_PRICE_ID, + ATTR_VALUE: 1.1, + }, + blocking=True, ) - mock_request.post( - "https://api.wall-box.com/chargers/config/12345", - json={CHARGER_ENERGY_PRICE_KEY: 1.1}, - status_code=403, - ) - - with pytest.raises(ConfigEntryAuthFailed): - await hass.services.async_call( - "number", - SERVICE_SET_VALUE, - { - ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ENERGY_PRICE_ID, - ATTR_VALUE: 1.1, - }, - blocking=True, - ) async def test_wallbox_number_class_platform_not_ready( @@ -218,19 +278,16 @@ async def test_wallbox_number_class_icp_energy( await setup_integration(hass, entry) - with requests_mock.Mocker() as mock_request: - mock_request.get( - "https://user-api.wall-box.com/users/signin", - json=authorisation_response, - status_code=200, - ) - - mock_request.post( - "https://api.wall-box.com/chargers/config/12345", - json={CHARGER_MAX_ICP_CURRENT_KEY: 10}, - status_code=200, - ) - + with ( + patch( + "homeassistant.components.wallbox.Wallbox.authenticate", + new=Mock(return_value=authorisation_response), + ), + patch( + "homeassistant.components.wallbox.Wallbox.setIcpMaxCurrent", + new=Mock(return_value={CHARGER_MAX_ICP_CURRENT_KEY: 10}), + ), + ): await hass.services.async_call( NUMBER_DOMAIN, SERVICE_SET_VALUE, @@ -249,28 +306,26 @@ async def test_wallbox_number_class_icp_energy_auth_error( await setup_integration(hass, entry) - with requests_mock.Mocker() as mock_request: - mock_request.get( - "https://user-api.wall-box.com/users/signin", - json=authorisation_response, - status_code=200, + with ( + patch( + "homeassistant.components.wallbox.Wallbox.authenticate", + new=Mock(return_value=authorisation_response), + ), + patch( + "homeassistant.components.wallbox.Wallbox.setIcpMaxCurrent", + new=Mock(side_effect=http_403_error), + ), + pytest.raises(InvalidAuth), + ): + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ICP_CURRENT_ID, + ATTR_VALUE: 10, + }, + blocking=True, ) - mock_request.post( - "https://api.wall-box.com/chargers/config/12345", - json={CHARGER_MAX_ICP_CURRENT_KEY: 10}, - status_code=403, - ) - - with pytest.raises(InvalidAuth): - await hass.services.async_call( - NUMBER_DOMAIN, - SERVICE_SET_VALUE, - { - ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ICP_CURRENT_ID, - ATTR_VALUE: 10, - }, - blocking=True, - ) async def test_wallbox_number_class_icp_energy_connection_error( @@ -280,25 +335,52 @@ async def test_wallbox_number_class_icp_energy_connection_error( await setup_integration(hass, entry) - with requests_mock.Mocker() as mock_request: - mock_request.get( - "https://user-api.wall-box.com/users/signin", - json=authorisation_response, - status_code=200, - ) - mock_request.post( - "https://api.wall-box.com/chargers/config/12345", - json={CHARGER_MAX_ICP_CURRENT_KEY: 10}, - status_code=404, + with ( + patch( + "homeassistant.components.wallbox.Wallbox.authenticate", + new=Mock(return_value=authorisation_response), + ), + patch( + "homeassistant.components.wallbox.Wallbox.setIcpMaxCurrent", + new=Mock(side_effect=http_404_error), + ), + pytest.raises(HomeAssistantError), + ): + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ICP_CURRENT_ID, + ATTR_VALUE: 10, + }, + blocking=True, ) - with pytest.raises(ConnectionError): - await hass.services.async_call( - NUMBER_DOMAIN, - SERVICE_SET_VALUE, - { - ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ICP_CURRENT_ID, - ATTR_VALUE: 10, - }, - blocking=True, - ) + +async def test_wallbox_number_class_icp_energy_too_many_request( + hass: HomeAssistant, entry: MockConfigEntry +) -> None: + """Test wallbox sensor class.""" + + await setup_integration(hass, entry) + + with ( + patch( + "homeassistant.components.wallbox.Wallbox.authenticate", + new=Mock(return_value=authorisation_response), + ), + patch( + "homeassistant.components.wallbox.Wallbox.setIcpMaxCurrent", + new=Mock(side_effect=http_429_error), + ), + pytest.raises(HomeAssistantError), + ): + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ICP_CURRENT_ID, + ATTR_VALUE: 10, + }, + blocking=True, + ) diff --git a/tests/components/wallbox/test_select.py b/tests/components/wallbox/test_select.py index 516b1e87c27..f59a8367b41 100644 --- a/tests/components/wallbox/test_select.py +++ b/tests/components/wallbox/test_select.py @@ -16,6 +16,7 @@ from homeassistant.core import HomeAssistant, HomeAssistantError from . import ( authorisation_response, http_404_error, + http_429_error, setup_integration_select, test_response, test_response_eco_mode, @@ -109,7 +110,41 @@ async def test_wallbox_select_class_error( "homeassistant.components.wallbox.Wallbox.enableEcoSmart", new=Mock(side_effect=error), ), - pytest.raises(HomeAssistantError, match="Error communicating with Wallbox API"), + pytest.raises(HomeAssistantError), + ): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: MOCK_SELECT_ENTITY_ID, + ATTR_OPTION: mode, + }, + blocking=True, + ) + + +@pytest.mark.parametrize(("mode", "response"), TEST_OPTIONS) +async def test_wallbox_select_too_many_requests_error( + hass: HomeAssistant, + entry: MockConfigEntry, + mode, + response, + mock_authenticate, +) -> None: + """Test wallbox select class connection error.""" + + await setup_integration_select(hass, entry, response) + + with ( + patch( + "homeassistant.components.wallbox.Wallbox.disableEcoSmart", + new=Mock(side_effect=http_429_error), + ), + patch( + "homeassistant.components.wallbox.Wallbox.enableEcoSmart", + new=Mock(side_effect=http_429_error), + ), + pytest.raises(HomeAssistantError), ): await hass.services.async_call( SELECT_DOMAIN, diff --git a/tests/components/wallbox/test_switch.py b/tests/components/wallbox/test_switch.py index b7c3a81dc73..eb983ca44ce 100644 --- a/tests/components/wallbox/test_switch.py +++ b/tests/components/wallbox/test_switch.py @@ -1,15 +1,16 @@ """Test Wallbox Lock component.""" +from unittest.mock import Mock, patch + import pytest -import requests_mock from homeassistant.components.switch import SERVICE_TURN_OFF, SERVICE_TURN_ON from homeassistant.components.wallbox.const import CHARGER_STATUS_ID_KEY from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.exceptions import HomeAssistantError -from . import authorisation_response, setup_integration +from . import authorisation_response, http_404_error, http_429_error, setup_integration from .const import MOCK_SWITCH_ENTITY_ID from tests.common import MockConfigEntry @@ -26,18 +27,20 @@ async def test_wallbox_switch_class( assert state assert state.state == "on" - with requests_mock.Mocker() as mock_request: - mock_request.get( - "https://user-api.wall-box.com/users/signin", - json=authorisation_response, - status_code=200, - ) - mock_request.post( - "https://api.wall-box.com/v3/chargers/12345/remote-action", - json={CHARGER_STATUS_ID_KEY: 193}, - status_code=200, - ) - + with ( + patch( + "homeassistant.components.wallbox.Wallbox.authenticate", + new=Mock(return_value=authorisation_response), + ), + patch( + "homeassistant.components.wallbox.Wallbox.pauseChargingSession", + new=Mock(return_value={CHARGER_STATUS_ID_KEY: 193}), + ), + patch( + "homeassistant.components.wallbox.Wallbox.resumeChargingSession", + new=Mock(return_value={CHARGER_STATUS_ID_KEY: 193}), + ), + ): await hass.services.async_call( "switch", SERVICE_TURN_ON, @@ -64,72 +67,52 @@ async def test_wallbox_switch_class_connection_error( await setup_integration(hass, entry) - with requests_mock.Mocker() as mock_request: - mock_request.get( - "https://user-api.wall-box.com/users/signin", - json=authorisation_response, - status_code=200, - ) - mock_request.post( - "https://api.wall-box.com/v3/chargers/12345/remote-action", - json={CHARGER_STATUS_ID_KEY: 193}, - status_code=404, + with ( + patch( + "homeassistant.components.wallbox.Wallbox.authenticate", + new=Mock(return_value=authorisation_response), + ), + patch( + "homeassistant.components.wallbox.Wallbox.resumeChargingSession", + new=Mock(side_effect=http_404_error), + ), + pytest.raises(HomeAssistantError), + ): + # Test behavior when a connection error occurs + await hass.services.async_call( + "switch", + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: MOCK_SWITCH_ENTITY_ID, + }, + blocking=True, ) - with pytest.raises(ConnectionError): - await hass.services.async_call( - "switch", - SERVICE_TURN_ON, - { - ATTR_ENTITY_ID: MOCK_SWITCH_ENTITY_ID, - }, - blocking=True, - ) - with pytest.raises(ConnectionError): - await hass.services.async_call( - "switch", - SERVICE_TURN_OFF, - { - ATTR_ENTITY_ID: MOCK_SWITCH_ENTITY_ID, - }, - blocking=True, - ) - -async def test_wallbox_switch_class_authentication_error( +async def test_wallbox_switch_class_too_many_requests( hass: HomeAssistant, entry: MockConfigEntry ) -> None: """Test wallbox switch class connection error.""" await setup_integration(hass, entry) - with requests_mock.Mocker() as mock_request: - mock_request.get( - "https://user-api.wall-box.com/users/signin", - json=authorisation_response, - status_code=200, + with ( + patch( + "homeassistant.components.wallbox.Wallbox.authenticate", + new=Mock(return_value=authorisation_response), + ), + patch( + "homeassistant.components.wallbox.Wallbox.resumeChargingSession", + new=Mock(side_effect=http_429_error), + ), + pytest.raises(HomeAssistantError), + ): + # Test behavior when a connection error occurs + await hass.services.async_call( + "switch", + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: MOCK_SWITCH_ENTITY_ID, + }, + blocking=True, ) - mock_request.post( - "https://api.wall-box.com/v3/chargers/12345/remote-action", - json={CHARGER_STATUS_ID_KEY: 193}, - status_code=403, - ) - - with pytest.raises(ConfigEntryAuthFailed): - await hass.services.async_call( - "switch", - SERVICE_TURN_ON, - { - ATTR_ENTITY_ID: MOCK_SWITCH_ENTITY_ID, - }, - blocking=True, - ) - with pytest.raises(ConfigEntryAuthFailed): - await hass.services.async_call( - "switch", - SERVICE_TURN_OFF, - { - ATTR_ENTITY_ID: MOCK_SWITCH_ENTITY_ID, - }, - blocking=True, - ) From ddf8e0de4bed1d96c39b0d6bfb01b0d7645eed60 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Fri, 20 Jun 2025 20:22:33 +0200 Subject: [PATCH 256/266] Bump deebot-client to 13.4.0 (#147221) --- homeassistant/components/ecovacs/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ecovacs/manifest.json b/homeassistant/components/ecovacs/manifest.json index 8a7388da735..97739f698d9 100644 --- a/homeassistant/components/ecovacs/manifest.json +++ b/homeassistant/components/ecovacs/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/ecovacs", "iot_class": "cloud_push", "loggers": ["sleekxmppfs", "sucks", "deebot_client"], - "requirements": ["py-sucks==0.9.11", "deebot-client==13.3.0"] + "requirements": ["py-sucks==0.9.11", "deebot-client==13.4.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 11bb82d6557..549ff32cd2a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -765,7 +765,7 @@ debugpy==1.8.14 # decora==0.6 # homeassistant.components.ecovacs -deebot-client==13.3.0 +deebot-client==13.4.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns diff --git a/requirements_test_all.txt b/requirements_test_all.txt index daf7193ee3a..d366dd5c92d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -665,7 +665,7 @@ debugpy==1.8.14 # decora==0.6 # homeassistant.components.ecovacs -deebot-client==13.3.0 +deebot-client==13.4.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns From 60be2cb1683d9ede1ca5819ef993bda867426bb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sat, 21 Jun 2025 10:53:17 +0100 Subject: [PATCH 257/266] Handle the new JSON payload from traccar clients (#147254) --- homeassistant/components/traccar/__init__.py | 42 ++++++++++++++--- homeassistant/components/traccar/const.py | 1 - tests/components/traccar/test_init.py | 47 +++++++++++++++++++- 3 files changed, 82 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/traccar/__init__.py b/homeassistant/components/traccar/__init__.py index 5b9bc2551b7..e8c151179ce 100644 --- a/homeassistant/components/traccar/__init__.py +++ b/homeassistant/components/traccar/__init__.py @@ -1,9 +1,12 @@ """Support for Traccar Client.""" from http import HTTPStatus +from json import JSONDecodeError +import logging from aiohttp import web import voluptuous as vol +from voluptuous.humanize import humanize_error from homeassistant.components import webhook from homeassistant.config_entries import ConfigEntry @@ -20,7 +23,6 @@ from .const import ( ATTR_LATITUDE, ATTR_LONGITUDE, ATTR_SPEED, - ATTR_TIMESTAMP, DOMAIN, ) @@ -29,6 +31,7 @@ PLATFORMS = [Platform.DEVICE_TRACKER] TRACKER_UPDATE = f"{DOMAIN}_tracker_update" +LOGGER = logging.getLogger(__name__) DEFAULT_ACCURACY = 200 DEFAULT_BATTERY = -1 @@ -49,21 +52,50 @@ WEBHOOK_SCHEMA = vol.Schema( vol.Optional(ATTR_BATTERY, default=DEFAULT_BATTERY): vol.Coerce(float), vol.Optional(ATTR_BEARING): vol.Coerce(float), vol.Optional(ATTR_SPEED): vol.Coerce(float), - vol.Optional(ATTR_TIMESTAMP): vol.Coerce(int), }, extra=vol.REMOVE_EXTRA, ) +def _parse_json_body(json_body: dict) -> dict: + """Parse JSON body from request.""" + location = json_body.get("location", {}) + coords = location.get("coords", {}) + battery_level = location.get("battery", {}).get("level") + return { + "id": json_body.get("device_id"), + "lat": coords.get("latitude"), + "lon": coords.get("longitude"), + "accuracy": coords.get("accuracy"), + "altitude": coords.get("altitude"), + "batt": battery_level * 100 if battery_level is not None else DEFAULT_BATTERY, + "bearing": coords.get("heading"), + "speed": coords.get("speed"), + } + + async def handle_webhook( - hass: HomeAssistant, webhook_id: str, request: web.Request + hass: HomeAssistant, + webhook_id: str, + request: web.Request, ) -> web.Response: """Handle incoming webhook with Traccar Client request.""" + if not (requestdata := dict(request.query)): + try: + requestdata = _parse_json_body(await request.json()) + except JSONDecodeError as error: + LOGGER.error("Error parsing JSON body: %s", error) + return web.Response( + text="Invalid JSON", + status=HTTPStatus.UNPROCESSABLE_ENTITY, + ) try: - data = WEBHOOK_SCHEMA(dict(request.query)) + data = WEBHOOK_SCHEMA(requestdata) except vol.MultipleInvalid as error: + LOGGER.warning(humanize_error(requestdata, error)) return web.Response( - text=error.error_message, status=HTTPStatus.UNPROCESSABLE_ENTITY + text=error.error_message, + status=HTTPStatus.UNPROCESSABLE_ENTITY, ) attrs = { diff --git a/homeassistant/components/traccar/const.py b/homeassistant/components/traccar/const.py index df4bfa8ec99..f6928cc9ee9 100644 --- a/homeassistant/components/traccar/const.py +++ b/homeassistant/components/traccar/const.py @@ -17,7 +17,6 @@ ATTR_LONGITUDE = "lon" ATTR_MOTION = "motion" ATTR_SPEED = "speed" ATTR_STATUS = "status" -ATTR_TIMESTAMP = "timestamp" ATTR_TRACKER = "tracker" ATTR_TRACCAR_ID = "traccar_id" diff --git a/tests/components/traccar/test_init.py b/tests/components/traccar/test_init.py index fb90262a084..eb864cadd87 100644 --- a/tests/components/traccar/test_init.py +++ b/tests/components/traccar/test_init.py @@ -146,8 +146,12 @@ async def test_enter_and_exit( assert len(entity_registry.entities) == 1 -async def test_enter_with_attrs(hass: HomeAssistant, client, webhook_id) -> None: - """Test when additional attributes are present.""" +async def test_enter_with_attrs_as_query( + hass: HomeAssistant, + client, + webhook_id, +) -> None: + """Test when additional attributes are present URL query.""" url = f"/api/webhook/{webhook_id}" data = { "timestamp": 123456789, @@ -197,6 +201,45 @@ async def test_enter_with_attrs(hass: HomeAssistant, client, webhook_id) -> None assert state.attributes["altitude"] == 123 +async def test_enter_with_attrs_as_payload( + hass: HomeAssistant, client, webhook_id +) -> None: + """Test when additional attributes are present in JSON payload.""" + url = f"/api/webhook/{webhook_id}" + data = { + "location": { + "coords": { + "heading": "105.32", + "latitude": "1.0", + "longitude": "1.1", + "accuracy": 10.5, + "altitude": 102.0, + "speed": 100.0, + }, + "extras": {}, + "manual": True, + "is_moving": False, + "_": "&id=123&lat=1.0&lon=1.1×tamp=2013-09-17T07:32:51Z&", + "odometer": 0, + "activity": {"type": "still"}, + "timestamp": "2013-09-17T07:32:51Z", + "battery": {"level": 0.1, "is_charging": False}, + }, + "device_id": "123", + } + + req = await client.post(url, json=data) + await hass.async_block_till_done() + assert req.status == HTTPStatus.OK + state = hass.states.get(f"{DEVICE_TRACKER_DOMAIN}.{data['device_id']}") + assert state.state == STATE_NOT_HOME + assert state.attributes["gps_accuracy"] == 10.5 + assert state.attributes["battery_level"] == 10.0 + assert state.attributes["speed"] == 100.0 + assert state.attributes["bearing"] == 105.32 + assert state.attributes["altitude"] == 102.0 + + async def test_two_devices(hass: HomeAssistant, client, webhook_id) -> None: """Test updating two different devices.""" url = f"/api/webhook/{webhook_id}" From f7d933444526e34435e84642ab1e7278d7e12e8c Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sat, 21 Jun 2025 16:44:22 +0300 Subject: [PATCH 258/266] Bump aioamazondevices to 3.1.14 (#147257) --- 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 aeecb5bc96c..a2bb423860b 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.1.12"] + "requirements": ["aioamazondevices==3.1.14"] } diff --git a/requirements_all.txt b/requirements_all.txt index 549ff32cd2a..4ccd6a41826 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -182,7 +182,7 @@ aioairzone-cloud==0.6.12 aioairzone==1.0.0 # homeassistant.components.alexa_devices -aioamazondevices==3.1.12 +aioamazondevices==3.1.14 # homeassistant.components.ambient_network # homeassistant.components.ambient_station diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d366dd5c92d..9f4ff3f88f9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -170,7 +170,7 @@ aioairzone-cloud==0.6.12 aioairzone==1.0.0 # homeassistant.components.alexa_devices -aioamazondevices==3.1.12 +aioamazondevices==3.1.14 # homeassistant.components.ambient_network # homeassistant.components.ambient_station From f9d5cb957f9300b7a9260587c501d1ab7d555fc9 Mon Sep 17 00:00:00 2001 From: Raphael Hehl <7577984+RaHehl@users.noreply.github.com> Date: Sun, 22 Jun 2025 02:14:26 +0200 Subject: [PATCH 259/266] Bump uiprotect to version 7.14.1 (#147280) --- 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 f99d910adf9..47e2a01e798 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.0", "unifi-discovery==1.2.0"], + "requirements": ["uiprotect==7.14.1", "unifi-discovery==1.2.0"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 4ccd6a41826..4d365187ac1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2987,7 +2987,7 @@ typedmonarchmoney==0.4.4 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==7.14.0 +uiprotect==7.14.1 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9f4ff3f88f9..b29649985c3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2458,7 +2458,7 @@ typedmonarchmoney==0.4.4 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==7.14.0 +uiprotect==7.14.1 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From 9d0701198f339a526a1f64b6c67a80dead9fa54f Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 18 Jun 2025 15:49:44 -0500 Subject: [PATCH 260/266] Bump aioesphomeapi to 32.2.4 (#147100) Bump aioesphomeapi --- 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 9b70aba4de1..6142b9ce5ec 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==32.2.1", + "aioesphomeapi==32.2.4", "esphome-dashboard-api==1.3.0", "bleak-esphome==2.16.0" ], diff --git a/requirements_all.txt b/requirements_all.txt index 4d365187ac1..7a36e7d7c7d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -244,7 +244,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==32.2.1 +aioesphomeapi==32.2.4 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b29649985c3..126585e626a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -232,7 +232,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==32.2.1 +aioesphomeapi==32.2.4 # homeassistant.components.flo aioflo==2021.11.0 From 105618734c6631573fbbdeb89d5af71c9d3d21e7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Jun 2025 16:24:16 +0200 Subject: [PATCH 261/266] Bump aioesphomeapi to 33.0.0 (#147296) fixes compat warning with protobuf 6.x changelog: https://github.com/esphome/aioesphomeapi/compare/v32.2.4...v33.0.0 Not a breaking change for HA since we are already on protobuf 6 --- 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 6142b9ce5ec..0577ed10c19 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==32.2.4", + "aioesphomeapi==33.0.0", "esphome-dashboard-api==1.3.0", "bleak-esphome==2.16.0" ], diff --git a/requirements_all.txt b/requirements_all.txt index 7a36e7d7c7d..2ce35c3fd8d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -244,7 +244,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==32.2.4 +aioesphomeapi==33.0.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 126585e626a..8ca017a72c2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -232,7 +232,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==32.2.4 +aioesphomeapi==33.0.0 # homeassistant.components.flo aioflo==2021.11.0 From dfd42863bb23464f9f0ffce56317ad4d04dd4c35 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 23 Jun 2025 15:38:16 +0300 Subject: [PATCH 262/266] Fix reload for Shelly devices with no script support (#147344) --- homeassistant/components/shelly/coordinator.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index cba559a9773..fa434588b34 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -835,6 +835,15 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): except InvalidAuthError: self.config_entry.async_start_reauth(self.hass) return + except RpcCallError as err: + # Ignore 404 (No handler for) error + if err.code != 404: + LOGGER.debug( + "Error during shutdown for device %s: %s", + self.name, + err.message, + ) + return except DeviceConnectionError as err: # If the device is restarting or has gone offline before # the ping/pong timeout happens, the shutdown command From 570315687f85a5daf3bf98c6bdc35dfa98749eee Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 23 Jun 2025 12:55:05 +0000 Subject: [PATCH 263/266] Bump version to 2025.6.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 9d5ed69f8d7..6b51f735e97 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 = 6 -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 226420c7605..fc66f3f8d8c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.6.1" +version = "2025.6.2" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From 06ed452d8f738432d996540a8646e707890a7874 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 23 Jun 2025 16:06:19 +0200 Subject: [PATCH 264/266] Add Matter protocol to Switchbot (#147356) --- homeassistant/brands/switchbot.json | 3 ++- homeassistant/generated/integrations.json | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/brands/switchbot.json b/homeassistant/brands/switchbot.json index 0909b24a146..43963109ee7 100644 --- a/homeassistant/brands/switchbot.json +++ b/homeassistant/brands/switchbot.json @@ -1,5 +1,6 @@ { "domain": "switchbot", "name": "SwitchBot", - "integrations": ["switchbot", "switchbot_cloud"] + "integrations": ["switchbot", "switchbot_cloud"], + "iot_standards": ["matter"] } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 846a5c74ddb..40c1f873382 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -6426,7 +6426,10 @@ "iot_class": "cloud_polling", "name": "SwitchBot Cloud" } - } + }, + "iot_standards": [ + "matter" + ] }, "switcher_kis": { "name": "Switcher", From 2f89317fed76d5deb6d696033cc751273c88cbca Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 24 Jun 2025 10:49:51 +0200 Subject: [PATCH 265/266] Update frontend to 20250531.4 (#147414) --- 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 efb4891debf..d996963cb9c 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==20250531.3"] + "requirements": ["home-assistant-frontend==20250531.4"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 706218022b3..9bca440f136 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -39,7 +39,7 @@ habluetooth==3.49.0 hass-nabucasa==0.101.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250531.3 +home-assistant-frontend==20250531.4 home-assistant-intents==2025.6.10 httpx==0.28.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 2ce35c3fd8d..4426ebc94a3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1164,7 +1164,7 @@ hole==0.8.0 holidays==0.75 # homeassistant.components.frontend -home-assistant-frontend==20250531.3 +home-assistant-frontend==20250531.4 # homeassistant.components.conversation home-assistant-intents==2025.6.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8ca017a72c2..0845cd0ea1e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1010,7 +1010,7 @@ hole==0.8.0 holidays==0.75 # homeassistant.components.frontend -home-assistant-frontend==20250531.3 +home-assistant-frontend==20250531.4 # homeassistant.components.conversation home-assistant-intents==2025.6.10 From 94fd9d165786110061495e4f9e5e9a7021295658 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 24 Jun 2025 11:09:26 +0000 Subject: [PATCH 266/266] Bump version to 2025.6.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 6b51f735e97..c006137a024 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 = 6 -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 fc66f3f8d8c..946cb224943 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.6.2" +version = "2025.6.3" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3."