From 480706889fd2c186d689e04d551c6846bf3f2a3e Mon Sep 17 00:00:00 2001 From: Martin Hoefling Date: Sun, 2 Aug 2026 20:26:55 +0200 Subject: [PATCH 1/4] =?UTF-8?q?Bump=20KNX=20dependencies=20(xknx=203.18.0,?= =?UTF-8?q?=20xknxproject=203.10.0,=20knx-telegram-=E2=80=A6=20(#178031)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- homeassistant/components/knx/manifest.json | 6 +++--- requirements_all.txt | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 70193300048744..4f4def15dff051 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -11,10 +11,10 @@ "loggers": ["xknx", "xknxproject"], "quality_scale": "platinum", "requirements": [ - "xknx==3.17.0", - "xknxproject==3.9.0", + "xknx==3.18.0", + "xknxproject==3.10.0", "knx-frontend==2026.7.23.145751", - "knx-telegram-store[sqlite,postgres]==0.11.1" + "knx-telegram-store[sqlite,postgres]==0.11.2" ], "single_config_entry": true } diff --git a/requirements_all.txt b/requirements_all.txt index 9b9d30e5ad1aff..56402a3c88f3ed 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1438,7 +1438,7 @@ knocki==0.4.2 knx-frontend==2026.7.23.145751 # homeassistant.components.knx -knx-telegram-store[sqlite,postgres]==0.11.1 +knx-telegram-store[sqlite,postgres]==0.11.2 # homeassistant.components.kraken krakenex==2.2.2 @@ -3406,10 +3406,10 @@ wyoming==1.10.0 xiaomi-ble==1.11.0 # homeassistant.components.knx -xknx==3.17.0 +xknx==3.18.0 # homeassistant.components.knx -xknxproject==3.9.0 +xknxproject==3.10.0 # homeassistant.components.fritz # homeassistant.components.rest From 5a55bfc57033c3d142f7b43eb8d8d0cd4d32d340 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Sun, 2 Aug 2026 22:06:42 +0200 Subject: [PATCH 2/4] Fix Hue MotionAware motion sensors not reporting motion (#177643) --- .../components/hue/v2/binary_sensor.py | 57 +++- tests/components/hue/test_binary_sensor.py | 256 +++++++++++++++++- 2 files changed, 302 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/hue/v2/binary_sensor.py b/homeassistant/components/hue/v2/binary_sensor.py index 739ae81b09bf18..d5a39fd6d4c297 100644 --- a/homeassistant/components/hue/v2/binary_sensor.py +++ b/homeassistant/components/hue/v2/binary_sensor.py @@ -19,9 +19,11 @@ ) from aiohue.v2.models.camera_motion import CameraMotion from aiohue.v2.models.contact import Contact, ContactState +from aiohue.v2.models.convenience_area_motion import ConvenienceAreaMotion from aiohue.v2.models.entertainment_configuration import EntertainmentStatus from aiohue.v2.models.grouped_motion import GroupedMotion from aiohue.v2.models.motion import Motion +from aiohue.v2.models.motion_area_configuration import MotionAreaHealth from aiohue.v2.models.resource import ResourceTypes from aiohue.v2.models.security_area_motion import SecurityAreaMotion from aiohue.v2.models.tamper import Tamper, TamperState @@ -181,11 +183,10 @@ def __init__( class HueMotionAwareSensor(HueMotionSensor): """Representation of a Motion sensor based on Hue Motion Aware. - Note that we only create sensors for the SecurityAreaMotion resource - and not for the ConvenienceAreaMotion resource, because the latter - does not have a state when it's not directly controlling lights. - The SecurityAreaMotion resource is always available with a state, allowing - Home Assistant users to actually use it as a motion sensor in their HA automations. + A MotionAware zone owns both a ConvenienceAreaMotion and a SecurityAreaMotion + service, and which of the two carries the motion state depends on how the zone + is set up in the Hue app. The source is therefore resolved from the zone, and + the state is unknown while neither service reports a valid one. """ controller: SecurityAreaMotionController @@ -201,6 +202,26 @@ def name(self) -> str: """Return sensor name.""" return self.controller.get_motion_area_configuration(self.resource.id).name + @property + @override + def is_on(self) -> bool | None: + """Return true if the binary sensor is on.""" + zone = self._motion_area_configuration + # switching a zone off leaves the `enabled` flag of its services untouched, + # so the zone is the only reliable signal that it stopped reporting motion + if not zone.enabled or zone.health == MotionAreaHealth.NOT_RUNNING: + return None + # either service can be the one reporting a zone, with the other left holding + # a reading that no longer changes, so take the first that has a valid one. + # The convenience service comes first because a zone that is bound to lights + # reports on that service. + for source in (self._convenience_service, self.resource): + if source is None or not source.enabled or source.motion is None: + continue + if (value := source.motion.value) is not None: + return value + return None + def __init__( self, bridge: HueBridge, @@ -229,6 +250,32 @@ async def async_added_to_hass(self) -> None: self._handle_event, self._motion_area_configuration.id ) ) + # zones that are bound to lights report their motion on the convenience service + if (service_id := self._convenience_service_id) is not None: + self.async_on_remove( + self.bridge.api.sensors.convenience_area_motion.subscribe( + self._handle_event, service_id + ) + ) + + @property + def _convenience_service_id(self) -> str | None: + """Return the id of this zone's ConvenienceAreaMotion service, if it has one.""" + return next( + ( + service.rid + for service in self._motion_area_configuration.services + if service.rtype == ResourceTypes.CONVENIENCE_AREA_MOTION + ), + None, + ) + + @property + def _convenience_service(self) -> ConvenienceAreaMotion | None: + """Return this zone's ConvenienceAreaMotion resource, if the bridge has it.""" + if (service_id := self._convenience_service_id) is None: + return None + return self.bridge.api.sensors.convenience_area_motion.get(service_id) # pylint: disable-next=home-assistant-enforce-class-module diff --git a/tests/components/hue/test_binary_sensor.py b/tests/components/hue/test_binary_sensor.py index 45c0ce04cbb835..75de2af66f291f 100644 --- a/tests/components/hue/test_binary_sensor.py +++ b/tests/components/hue/test_binary_sensor.py @@ -1,7 +1,10 @@ """Philips Hue binary_sensor platform tests for V2 bridge/api.""" +from typing import Any from unittest.mock import Mock +import pytest + from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.util.json import JsonArrayType @@ -9,6 +12,60 @@ from .conftest import setup_platform from .const import FAKE_BINARY_SENSOR, FAKE_DEVICE, FAKE_ZIGBEE_CONNECTIVITY +MOTION_AWARE_ENTITY_ID = "binary_sensor.test_room_test_room_motion_aware_sensor_1" +MOTION_AREA_CONFIGURATION_ID = "5e6f7a8b-9c1d-4e2f-b3a4-5c6d7e8f9a0b" +AREA_MOTION_SERVICE_IDS = { + "convenience_area_motion": "4f317b69-9da0-4b4f-84f2-7ca07b9fe345", + "security_area_motion": "8b7e4f82-9c3d-4e1a-a5f6-8d9c7b2a3e4f", +} + +MOTION_DETECTED = { + "motion": True, + "motion_valid": True, + "motion_report": {"changed": "2023-09-23T08:20:51.384Z", "motion": True}, +} +MOTION_CLEARED = { + "motion": False, + "motion_valid": True, + "motion_report": {"changed": "2023-09-23T08:13:42.394Z", "motion": False}, +} +MOTION_INVALID = { + "motion": False, + "motion_valid": False, + "motion_report": {"changed": "2023-09-23T05:54:08.166Z", "motion": False}, +} + + +def area_motion_service( + service_type: str, + *, + enabled: bool = True, + motion: dict[str, Any] | None = None, +) -> dict[str, Any]: + """Build a service of the MotionAware zone, without `motion` when none is given.""" + service = { + "id": AREA_MOTION_SERVICE_IDS[service_type], + "owner": { + "rid": MOTION_AREA_CONFIGURATION_ID, + "rtype": "motion_area_configuration", + }, + "enabled": enabled, + "type": service_type, + } + if motion is not None: + service["motion"] = motion + return service + + +def replace_resources( + data: JsonArrayType, resources: list[dict[str, Any]] +) -> JsonArrayType: + """Return the test data with each resource of the same id replaced.""" + replacements = {resource["id"]: resource for resource in resources} + missing = replacements.keys() - {resource["id"] for resource in data} + assert not missing, f"resource id(s) not present in the test data: {missing}" + return [replacements.get(resource["id"], resource) for resource in data] + async def test_binary_sensors( hass: HomeAssistant, mock_bridge_v2: Mock, v2_resources_test_data: JsonArrayType @@ -88,7 +145,7 @@ async def test_binary_sensors( assert sensor.attributes["device_class"] == "motion" # test motion aware sensor - sensor = hass.states.get("binary_sensor.test_room_test_room_motion_aware_sensor_1") + sensor = hass.states.get(MOTION_AWARE_ENTITY_ID) assert sensor is not None assert sensor.state == "off" assert sensor.name == "Test Room Motion Aware Sensor 1" @@ -195,15 +252,17 @@ async def test_motion_aware_sensor( await setup_platform(hass, mock_bridge_v2, Platform.BINARY_SENSOR) # test motion aware sensor exists and has correct state - sensor = hass.states.get("binary_sensor.test_room_test_room_motion_aware_sensor_1") + sensor = hass.states.get(MOTION_AWARE_ENTITY_ID) assert sensor is not None assert sensor.state == "off" assert sensor.attributes["device_class"] == "motion" # test update of motion aware sensor works on incoming event + # the zone in the test data has its convenience service enabled, so that is the + # service reporting its motion updated_sensor = { - "id": "8b7e4f82-9c3d-4e1a-a5f6-8d9c7b2a3e4f", - "type": "security_area_motion", + "id": "4f317b69-9da0-4b4f-84f2-7ca07b9fe345", + "type": "convenience_area_motion", "motion": { "motion": True, "motion_valid": True, @@ -212,7 +271,7 @@ async def test_motion_aware_sensor( } mock_bridge_v2.api.emit_event("update", updated_sensor) await hass.async_block_till_done() - sensor = hass.states.get("binary_sensor.test_room_test_room_motion_aware_sensor_1") + sensor = hass.states.get(MOTION_AWARE_ENTITY_ID) assert sensor.state == "on" # test name update when motion area configuration name changes @@ -225,6 +284,191 @@ async def test_motion_aware_sensor( await hass.async_block_till_done() # The entity name is derived from the motion area configuration name # but the entity ID doesn't change - we just verify the sensor still exists - sensor = hass.states.get("binary_sensor.test_room_test_room_motion_aware_sensor_1") + sensor = hass.states.get(MOTION_AWARE_ENTITY_ID) assert sensor is not None assert sensor.name == "Test Room Updated Motion Area" + + +@pytest.mark.parametrize( + ("services", "expected_state"), + [ + pytest.param( + [ + area_motion_service("security_area_motion", motion=MOTION_CLEARED), + area_motion_service("convenience_area_motion", motion=MOTION_DETECTED), + ], + "on", + id="bound_to_lights_reads_convenience", + ), + pytest.param( + [ + area_motion_service("security_area_motion", motion=MOTION_CLEARED), + area_motion_service( + "convenience_area_motion", enabled=False, motion=MOTION_DETECTED + ), + ], + "off", + id="not_bound_to_lights_reads_security", + ), + pytest.param( + [ + area_motion_service("security_area_motion"), + area_motion_service("convenience_area_motion", motion=MOTION_DETECTED), + ], + "on", + id="hue_secure_security_without_motion_reads_convenience", + ), + pytest.param( + [ + area_motion_service("security_area_motion", motion=MOTION_INVALID), + area_motion_service( + "convenience_area_motion", enabled=False, motion=MOTION_DETECTED + ), + ], + "unknown", + id="not_bound_to_lights_without_valid_reading", + ), + # a real zone can have its convenience service enabled while only the security + # service reports, so an enabled service without a reading must not win + pytest.param( + [ + area_motion_service("security_area_motion", motion=MOTION_DETECTED), + area_motion_service("convenience_area_motion", motion=MOTION_INVALID), + ], + "on", + id="falls_back_to_security_when_convenience_has_no_reading", + ), + ], +) +async def test_motion_aware_sensor_motion_source( + hass: HomeAssistant, + mock_bridge_v2: Mock, + v2_resources_test_data: JsonArrayType, + services: list[dict[str, Any]], + expected_state: str, +) -> None: + """Test the MotionAware sensor reads the zone service that reports motion.""" + # every case gives the service that must be ignored the opposite state, so + # reading the wrong one results in a state other than the asserted one + await mock_bridge_v2.api.load_test_data( + replace_resources(v2_resources_test_data, services) + ) + await setup_platform(hass, mock_bridge_v2, Platform.BINARY_SENSOR) + + assert hass.states.get(MOTION_AWARE_ENTITY_ID).state == expected_state + + +async def test_motion_aware_sensor_follows_convenience_service( + hass: HomeAssistant, mock_bridge_v2: Mock, v2_resources_test_data: JsonArrayType +) -> None: + """Test the MotionAware sensor updates on events of the convenience service.""" + await mock_bridge_v2.api.load_test_data( + replace_resources( + v2_resources_test_data, + [ + area_motion_service("security_area_motion"), + area_motion_service("convenience_area_motion", motion=MOTION_CLEARED), + ], + ) + ) + await setup_platform(hass, mock_bridge_v2, Platform.BINARY_SENSOR) + + assert hass.states.get(MOTION_AWARE_ENTITY_ID).state == "off" + + mock_bridge_v2.api.emit_event( + "update", + area_motion_service("convenience_area_motion", motion=MOTION_DETECTED), + ) + await hass.async_block_till_done() + assert hass.states.get(MOTION_AWARE_ENTITY_ID).state == "on" + + +async def test_motion_aware_sensor_follows_security_service( + hass: HomeAssistant, mock_bridge_v2: Mock, v2_resources_test_data: JsonArrayType +) -> None: + """Test the MotionAware sensor updates on events of the security service.""" + await mock_bridge_v2.api.load_test_data( + replace_resources( + v2_resources_test_data, + [ + area_motion_service("security_area_motion", motion=MOTION_CLEARED), + area_motion_service( + "convenience_area_motion", enabled=False, motion=MOTION_CLEARED + ), + ], + ) + ) + await setup_platform(hass, mock_bridge_v2, Platform.BINARY_SENSOR) + + assert hass.states.get(MOTION_AWARE_ENTITY_ID).state == "off" + + mock_bridge_v2.api.emit_event( + "update", + area_motion_service("security_area_motion", motion=MOTION_DETECTED), + ) + await hass.async_block_till_done() + assert hass.states.get(MOTION_AWARE_ENTITY_ID).state == "on" + + +async def test_motion_aware_sensor_without_convenience_resource( + hass: HomeAssistant, mock_bridge_v2: Mock, v2_resources_test_data: JsonArrayType +) -> None: + """Test the MotionAware sensor works when the convenience service is missing.""" + # the zone still lists the service, but the bridge never delivered the resource + data = replace_resources( + v2_resources_test_data, + [area_motion_service("security_area_motion", motion=MOTION_DETECTED)], + ) + convenience_id = AREA_MOTION_SERVICE_IDS["convenience_area_motion"] + await mock_bridge_v2.api.load_test_data( + [resource for resource in data if resource["id"] != convenience_id] + ) + await setup_platform(hass, mock_bridge_v2, Platform.BINARY_SENSOR) + + assert hass.states.get(MOTION_AWARE_ENTITY_ID).state == "on" + + +@pytest.mark.parametrize( + ("zone_update", "zone_restore"), + [ + pytest.param({"enabled": False}, {"enabled": True}, id="zone_switched_off"), + pytest.param( + {"health": "not_running"}, {"health": "healthy"}, id="zone_not_running" + ), + ], +) +async def test_motion_aware_sensor_zone_not_reporting( + hass: HomeAssistant, + mock_bridge_v2: Mock, + v2_resources_test_data: JsonArrayType, + zone_update: dict[str, Any], + zone_restore: dict[str, Any], +) -> None: + """Test the MotionAware sensor reports unknown while its zone is not reporting.""" + await mock_bridge_v2.api.load_test_data(v2_resources_test_data) + await setup_platform(hass, mock_bridge_v2, Platform.BINARY_SENSOR) + + assert hass.states.get(MOTION_AWARE_ENTITY_ID).state == "off" + + # the services keep reporting a valid state while the zone itself does not + mock_bridge_v2.api.emit_event( + "update", + { + "id": MOTION_AREA_CONFIGURATION_ID, + "type": "motion_area_configuration", + **zone_update, + }, + ) + await hass.async_block_till_done() + assert hass.states.get(MOTION_AWARE_ENTITY_ID).state == "unknown" + + mock_bridge_v2.api.emit_event( + "update", + { + "id": MOTION_AREA_CONFIGURATION_ID, + "type": "motion_area_configuration", + **zone_restore, + }, + ) + await hass.async_block_till_done() + assert hass.states.get(MOTION_AWARE_ENTITY_ID).state == "off" From 40b10b8c07928f7ea725d033a849942af5b4481d Mon Sep 17 00:00:00 2001 From: Amit Krishna <218109745+amitkio@users.noreply.github.com> Date: Mon, 3 Aug 2026 01:46:20 +0530 Subject: [PATCH 3/4] Bump energieleser to 0.1.6 (#177645) --- homeassistant/components/energieleser/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/energieleser/manifest.json b/homeassistant/components/energieleser/manifest.json index 72b5f99f0cd957..933ee9ada77014 100644 --- a/homeassistant/components/energieleser/manifest.json +++ b/homeassistant/components/energieleser/manifest.json @@ -7,7 +7,7 @@ "integration_type": "device", "iot_class": "local_polling", "quality_scale": "platinum", - "requirements": ["energieleser==0.1.5"], + "requirements": ["energieleser==0.1.6"], "zeroconf": [ { "type": "_stromleser._tcp.local." diff --git a/requirements_all.txt b/requirements_all.txt index 56402a3c88f3ed..5c20c424a5e298 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -922,7 +922,7 @@ emoji==2.8.0 emulated-roku==0.3.0 # homeassistant.components.energieleser -energieleser==0.1.5 +energieleser==0.1.6 # homeassistant.components.huisbaasje energyflip-client==0.2.2 From 08cdc4ac276f2441ae3c80c561fcd13b9e0941dc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Aug 2026 15:34:06 -0500 Subject: [PATCH 4/4] Bump esphome-dashboard-api to 1.4.0 (#178038) --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index d09e7d7f648199..e86f6ce3f1be1d 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -18,7 +18,7 @@ "quality_scale": "platinum", "requirements": [ "aioesphomeapi==45.6.1", - "esphome-dashboard-api==1.3.0", + "esphome-dashboard-api==1.4.0", "bleak-esphome==3.9.7" ], "zeroconf": ["_esphomelib._tcp.local."] diff --git a/requirements_all.txt b/requirements_all.txt index 5c20c424a5e298..b61f572ace203c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -961,7 +961,7 @@ eq3btsmart==2.3.0 esios_api==4.4.0 # homeassistant.components.esphome -esphome-dashboard-api==1.3.0 +esphome-dashboard-api==1.4.0 # homeassistant.components.essent essent-dynamic-pricing==0.3.1