1
0
mirror of https://github.com/home-assistant/core.git synced 2026-05-08 17:49:37 +01:00

Add support for Hue MotionAware sensors (#152811)

Co-authored-by: Franck Nijhof <git@frenck.dev>
This commit is contained in:
Marcel van der Veldt
2025-09-23 22:52:09 +02:00
committed by GitHub
parent 60bf298ca6
commit 2008a73657
8 changed files with 569 additions and 19 deletions
+28 -3
View File
@@ -6,6 +6,7 @@ from typing import Any
from aiohue.v2 import HueBridgeV2
from aiohue.v2.controllers.events import EventType
from aiohue.v2.models.bell_button import BellButton
from aiohue.v2.models.button import Button
from aiohue.v2.models.relative_rotary import RelativeRotary, RelativeRotaryDirection
@@ -39,19 +40,27 @@ async def async_setup_entry(
@callback
def async_add_entity(
event_type: EventType,
resource: Button | RelativeRotary,
resource: Button | RelativeRotary | BellButton,
) -> None:
"""Add entity from Hue resource."""
if isinstance(resource, RelativeRotary):
async_add_entities(
[HueRotaryEventEntity(bridge, api.sensors.relative_rotary, resource)]
)
elif isinstance(resource, BellButton):
async_add_entities(
[HueBellButtonEventEntity(bridge, api.sensors.bell_button, resource)]
)
else:
async_add_entities(
[HueButtonEventEntity(bridge, api.sensors.button, resource)]
)
for controller in (api.sensors.button, api.sensors.relative_rotary):
for controller in (
api.sensors.button,
api.sensors.relative_rotary,
api.sensors.bell_button,
):
# add all current items in controller
for item in controller:
async_add_entity(EventType.RESOURCE_ADDED, item)
@@ -67,6 +76,8 @@ async def async_setup_entry(
class HueButtonEventEntity(HueBaseEntity, EventEntity):
"""Representation of a Hue Event entity from a button resource."""
resource: Button | BellButton
entity_description = EventEntityDescription(
key="button",
device_class=EventDeviceClass.BUTTON,
@@ -91,7 +102,9 @@ class HueButtonEventEntity(HueBaseEntity, EventEntity):
}
@callback
def _handle_event(self, event_type: EventType, resource: Button) -> None:
def _handle_event(
self, event_type: EventType, resource: Button | BellButton
) -> None:
"""Handle status event for this resource (or it's parent)."""
if event_type == EventType.RESOURCE_UPDATED and resource.id == self.resource.id:
if resource.button is None or resource.button.button_report is None:
@@ -102,6 +115,18 @@ class HueButtonEventEntity(HueBaseEntity, EventEntity):
super()._handle_event(event_type, resource)
class HueBellButtonEventEntity(HueButtonEventEntity):
"""Representation of a Hue Event entity from a bell_button resource."""
resource: Button | BellButton
entity_description = EventEntityDescription(
key="bell_button",
device_class=EventDeviceClass.DOORBELL,
has_entity_name=True,
)
class HueRotaryEventEntity(HueBaseEntity, EventEntity):
"""Representation of a Hue Event entity from a RelativeRotary resource."""
@@ -13,13 +13,18 @@ from aiohue.v2.controllers.events import EventType
from aiohue.v2.controllers.sensors import (
CameraMotionController,
ContactController,
GroupedMotionController,
MotionController,
SecurityAreaMotionController,
TamperController,
)
from aiohue.v2.models.camera_motion import CameraMotion
from aiohue.v2.models.contact import Contact, ContactState
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.resource import ResourceTypes
from aiohue.v2.models.security_area_motion import SecurityAreaMotion
from aiohue.v2.models.tamper import Tamper, TamperState
from homeassistant.components.binary_sensor import (
@@ -29,21 +34,54 @@ from homeassistant.components.binary_sensor import (
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from ..bridge import HueConfigEntry
from ..bridge import HueBridge, HueConfigEntry
from ..const import DOMAIN
from .entity import HueBaseEntity
type SensorType = CameraMotion | Contact | Motion | EntertainmentConfiguration | Tamper
type SensorType = (
CameraMotion
| Contact
| Motion
| EntertainmentConfiguration
| Tamper
| GroupedMotion
| SecurityAreaMotion
)
type ControllerType = (
CameraMotionController
| ContactController
| MotionController
| EntertainmentConfigurationController
| TamperController
| GroupedMotionController
| SecurityAreaMotionController
)
def _resource_valid(resource: SensorType, controller: ControllerType) -> bool:
"""Return True if the resource is valid."""
if isinstance(resource, GroupedMotion):
# filter out GroupedMotion sensors that are not linked to a valid group/parent
if resource.owner.rtype not in (
ResourceTypes.ROOM,
ResourceTypes.ZONE,
ResourceTypes.SERVICE_GROUP,
):
return False
# guard against GroupedMotion without parent (should not happen, but just in case)
if not (parent := controller.get_parent(resource.id)):
return False
# filter out GroupedMotion sensors that have only one member, because Hue creates one
# default grouped Motion sensor per zone/room, which is not useful to expose in HA
if len(parent.children) <= 1:
return False
# default/other checks can go here (none for now)
return True
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HueConfigEntry,
@@ -59,11 +97,17 @@ async def async_setup_entry(
@callback
def async_add_sensor(event_type: EventType, resource: SensorType) -> None:
"""Add Hue Binary Sensor."""
"""Add Hue Binary Sensor from resource added callback."""
if not _resource_valid(resource, controller):
return
async_add_entities([make_binary_sensor_entity(resource)])
# add all current items in controller
async_add_entities(make_binary_sensor_entity(sensor) for sensor in controller)
async_add_entities(
make_binary_sensor_entity(sensor)
for sensor in controller
if _resource_valid(sensor, controller)
)
# register listener for new sensors
config_entry.async_on_unload(
@@ -78,6 +122,8 @@ async def async_setup_entry(
register_items(api.config.entertainment_configuration, HueEntertainmentActiveSensor)
register_items(api.sensors.contact, HueContactSensor)
register_items(api.sensors.tamper, HueTamperSensor)
register_items(api.sensors.grouped_motion, HueGroupedMotionSensor)
register_items(api.sensors.security_area_motion, HueMotionAwareSensor)
# pylint: disable-next=hass-enforce-class-module
@@ -102,6 +148,83 @@ class HueMotionSensor(HueBaseEntity, BinarySensorEntity):
return self.resource.motion.value
# pylint: disable-next=hass-enforce-class-module
class HueGroupedMotionSensor(HueMotionSensor):
"""Representation of a Hue Grouped Motion sensor."""
controller: GroupedMotionController
resource: GroupedMotion
def __init__(
self,
bridge: HueBridge,
controller: GroupedMotionController,
resource: GroupedMotion,
) -> None:
"""Initialize the sensor."""
super().__init__(bridge, controller, resource)
# link the GroupedMotion sensor to the parent the sensor is associated with
# which can either be a special ServiceGroup or a Zone/Room
parent = self.controller.get_parent(resource.id)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, parent.id)},
)
# pylint: disable-next=hass-enforce-class-module
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.
"""
controller: SecurityAreaMotionController
resource: SecurityAreaMotion
entity_description = BinarySensorEntityDescription(
key="motion_sensor",
device_class=BinarySensorDeviceClass.MOTION,
has_entity_name=False,
)
@property
def name(self) -> str:
"""Return sensor name."""
return self.controller.get_motion_area_configuration(self.resource.id).name
def __init__(
self,
bridge: HueBridge,
controller: SecurityAreaMotionController,
resource: SecurityAreaMotion,
) -> None:
"""Initialize the sensor."""
super().__init__(bridge, controller, resource)
# link the MotionAware sensor to the group the sensor is associated with
self._motion_area_configuration = self.controller.get_motion_area_configuration(
resource.id
)
group_id = self._motion_area_configuration.group.rid
self.group = self.bridge.api.groups[group_id]
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self.group.id)},
)
async def async_added_to_hass(self) -> None:
"""Call when entity is added."""
await super().async_added_to_hass()
# subscribe to updates of the MotionAreaConfiguration to update the name
self.async_on_remove(
self.bridge.api.config.subscribe(
self._handle_event, self._motion_area_configuration.id
)
)
# pylint: disable-next=hass-enforce-class-module
class HueEntertainmentActiveSensor(HueBaseEntity, BinarySensorEntity):
"""Representation of a Hue Entertainment Configuration as binary sensor."""
+7 -4
View File
@@ -9,6 +9,7 @@ from aiohue.v2.controllers.events import EventType
from aiohue.v2.controllers.groups import Room, Zone
from aiohue.v2.models.device import Device
from aiohue.v2.models.resource import ResourceTypes
from aiohue.v2.models.service_group import ServiceGroup
from homeassistant.const import (
ATTR_CONNECTIONS,
@@ -39,16 +40,16 @@ async def async_setup_devices(bridge: HueBridge):
dev_controller = api.devices
@callback
def add_device(hue_resource: Device | Room | Zone) -> dr.DeviceEntry:
def add_device(hue_resource: Device | Room | Zone | ServiceGroup) -> dr.DeviceEntry:
"""Register a Hue device in device registry."""
if isinstance(hue_resource, (Room, Zone)):
if isinstance(hue_resource, (Room, Zone, ServiceGroup)):
# Register a Hue Room/Zone as service in HA device registry.
return dev_reg.async_get_or_create(
config_entry_id=entry.entry_id,
entry_type=dr.DeviceEntryType.SERVICE,
identifiers={(DOMAIN, hue_resource.id)},
name=hue_resource.metadata.name,
model=hue_resource.type.value.title(),
model=hue_resource.type.value.replace("_", " ").title(),
manufacturer=api.config.bridge_device.product_data.manufacturer_name,
via_device=(DOMAIN, api.config.bridge_device.id),
suggested_area=hue_resource.metadata.name
@@ -85,7 +86,7 @@ async def async_setup_devices(bridge: HueBridge):
@callback
def handle_device_event(
evt_type: EventType, hue_resource: Device | Room | Zone
evt_type: EventType, hue_resource: Device | Room | Zone | ServiceGroup
) -> None:
"""Handle event from Hue controller."""
if evt_type == EventType.RESOURCE_DELETED:
@@ -101,6 +102,7 @@ async def async_setup_devices(bridge: HueBridge):
known_devices = [add_device(hue_device) for hue_device in hue_devices]
known_devices += [add_device(hue_room) for hue_room in api.groups.room]
known_devices += [add_device(hue_zone) for hue_zone in api.groups.zone]
known_devices += [add_device(sg) for sg in api.config.service_group]
# Check for nodes that no longer exist and remove them
for device in dr.async_entries_for_config_entry(dev_reg, entry.entry_id):
@@ -111,3 +113,4 @@ async def async_setup_devices(bridge: HueBridge):
entry.async_on_unload(dev_controller.subscribe(handle_device_event))
entry.async_on_unload(api.groups.room.subscribe(handle_device_event))
entry.async_on_unload(api.groups.zone.subscribe(handle_device_event))
entry.async_on_unload(api.config.service_group.subscribe(handle_device_event))
+67 -2
View File
@@ -9,13 +9,16 @@ from aiohue.v2 import HueBridgeV2
from aiohue.v2.controllers.events import EventType
from aiohue.v2.controllers.sensors import (
DevicePowerController,
GroupedLightLevelController,
LightLevelController,
SensorsController,
TemperatureController,
ZigbeeConnectivityController,
)
from aiohue.v2.models.device_power import DevicePower
from aiohue.v2.models.grouped_light_level import GroupedLightLevel
from aiohue.v2.models.light_level import LightLevel
from aiohue.v2.models.resource import ResourceTypes
from aiohue.v2.models.temperature import Temperature
from aiohue.v2.models.zigbee_connectivity import ZigbeeConnectivity
@@ -27,20 +30,50 @@ from homeassistant.components.sensor import (
)
from homeassistant.const import LIGHT_LUX, PERCENTAGE, EntityCategory, UnitOfTemperature
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from ..bridge import HueBridge, HueConfigEntry
from ..const import DOMAIN
from .entity import HueBaseEntity
type SensorType = DevicePower | LightLevel | Temperature | ZigbeeConnectivity
type SensorType = (
DevicePower | LightLevel | Temperature | ZigbeeConnectivity | GroupedLightLevel
)
type ControllerType = (
DevicePowerController
| LightLevelController
| TemperatureController
| ZigbeeConnectivityController
| GroupedLightLevelController
)
def _resource_valid(
resource: SensorType, controller: ControllerType, api: HueBridgeV2
) -> bool:
"""Return True if the resource is valid."""
if isinstance(resource, GroupedLightLevel):
# filter out GroupedLightLevel sensors that are not linked to a valid group/parent
if resource.owner.rtype not in (
ResourceTypes.ROOM,
ResourceTypes.ZONE,
ResourceTypes.SERVICE_GROUP,
):
return False
# guard against GroupedLightLevel without parent (should not happen, but just in case)
parent_id = resource.owner.rid
parent = api.groups.get(parent_id) or api.config.get(parent_id)
if not parent:
return False
# filter out GroupedLightLevel sensors that have only one member, because Hue creates one
# default grouped LightLevel sensor per zone/room, which is not useful to expose in HA
if len(parent.children) <= 1:
return False
# default/other checks can go here (none for now)
return True
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HueConfigEntry,
@@ -58,10 +91,16 @@ async def async_setup_entry(
@callback
def async_add_sensor(event_type: EventType, resource: SensorType) -> None:
"""Add Hue Sensor."""
if not _resource_valid(resource, controller, api):
return
async_add_entities([make_sensor_entity(resource)])
# add all current items in controller
async_add_entities(make_sensor_entity(sensor) for sensor in controller)
async_add_entities(
make_sensor_entity(sensor)
for sensor in controller
if _resource_valid(sensor, controller, api)
)
# register listener for new sensors
config_entry.async_on_unload(
@@ -75,6 +114,7 @@ async def async_setup_entry(
register_items(ctrl_base.light_level, HueLightLevelSensor)
register_items(ctrl_base.device_power, HueBatterySensor)
register_items(ctrl_base.zigbee_connectivity, HueZigbeeConnectivitySensor)
register_items(api.sensors.grouped_light_level, HueGroupedLightLevelSensor)
# pylint: disable-next=hass-enforce-class-module
@@ -140,6 +180,31 @@ class HueLightLevelSensor(HueSensorBase):
}
# pylint: disable-next=hass-enforce-class-module
class HueGroupedLightLevelSensor(HueLightLevelSensor):
"""Representation of a LightLevel (illuminance) sensor from a Hue GroupedLightLevel resource."""
controller: GroupedLightLevelController
resource: GroupedLightLevel
def __init__(
self,
bridge: HueBridge,
controller: GroupedLightLevelController,
resource: GroupedLightLevel,
) -> None:
"""Initialize the sensor."""
super().__init__(bridge, controller, resource)
# link the GroupedLightLevel sensor to the parent the sensor is associated with
# which can either be a special ServiceGroup or a Zone/Room
api = self.bridge.api
parent_id = resource.owner.rid
parent = api.groups.get(parent_id) or api.config.get(parent_id)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, parent.id)},
)
# pylint: disable-next=hass-enforce-class-module
class HueBatterySensor(HueSensorBase):
"""Representation of a Hue Battery sensor."""
@@ -2363,5 +2363,199 @@
"sensitivity_max": 4
},
"type": "motion"
},
{
"id": "4f317b69-9da0-4b4f-84f2-7ca07b9fe345",
"owner": {
"rid": "5e6f7a8b-9c1d-4e2f-b3a4-5c6d7e8f9a0b",
"rtype": "motion_area_configuration"
},
"enabled": true,
"motion": {
"motion": false,
"motion_valid": true,
"motion_report": {
"changed": "2023-09-23T08:13:42.394Z",
"motion": false
}
},
"sensitivity": {
"sensitivity": 2,
"sensitivity_max": 4
},
"type": "convenience_area_motion"
},
{
"id": "8b7e4f82-9c3d-4e1a-a5f6-8d9c7b2a3e4f",
"owner": {
"rid": "5e6f7a8b-9c1d-4e2f-b3a4-5c6d7e8f9a0b",
"rtype": "motion_area_configuration"
},
"enabled": true,
"motion": {
"motion": false,
"motion_valid": true,
"motion_report": {
"changed": "2023-09-23T05:54:08.166Z",
"motion": false
}
},
"sensitivity": {
"sensitivity": 2,
"sensitivity_max": 4
},
"type": "security_area_motion"
},
{
"id": "5e6f7a8b-9c1d-4e2f-b3a4-5c6d7e8f9a0b",
"name": "Motion Aware Sensor 1",
"group": {
"rid": "6ddc9066-7e7d-4a03-a773-c73937968296",
"rtype": "room"
},
"participants": [
{
"resource": {
"rid": "a17253ed-168d-471a-8e59-01a101441511",
"rtype": "motion_area_candidate"
},
"status": {
"health": "healthy"
}
}
],
"services": [
{
"rid": "4f317b69-9da0-4b4f-84f2-7ca07b9fe345",
"rtype": "convenience_area_motion"
},
{
"rid": "8b7e4f82-9c3d-4e1a-a5f6-8d9c7b2a3e4f",
"rtype": "security_area_motion"
}
],
"health": "healthy",
"enabled": true,
"type": "motion_area_configuration"
},
{
"id": "9f8e7d6c-5b4a-3e2d-1c0b-9a8f7e6d5c4b",
"owner": {
"rid": "3ff06175-29e8-44a8-8fe7-af591b0025da",
"rtype": "device"
},
"state": "no_update",
"problems": [],
"type": "device_software_update"
},
{
"id": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"owner": {
"rid": "4d5e6f7a-8b9c-0d1e-2f3a-4b5c6d7e8f9a",
"rtype": "service_group"
},
"enabled": true,
"light": {
"light_level_report": {
"changed": "2023-09-23T06:19:38.865Z",
"light_level": 0
}
},
"type": "grouped_light_level"
},
{
"id": "2b3c4d5e-6f7a-8b9c-0d1e-2f3a4b5c6d7e",
"owner": {
"rid": "4d5e6f7a-8b9c-0d1e-2f3a-4b5c6d7e8f9a",
"rtype": "service_group"
},
"enabled": true,
"motion": {
"motion_report": {
"changed": "2023-09-23T08:20:51.384Z",
"motion": false
}
},
"type": "grouped_motion"
},
{
"id": "3c4d5e6f-7a8b-9c0d-1e2f-3a4b5c6d7e8f",
"id_v1": "/sensors/75",
"owner": {
"rid": "3ff06175-29e8-44a8-8fe7-af591b0025da",
"rtype": "device"
},
"relative_rotary": {
"last_event": {
"action": "start",
"rotation": {
"direction": "clock_wise",
"steps": 30,
"duration": 400
}
},
"rotary_report": {
"updated": "2023-09-21T10:00:03.276Z",
"action": "start",
"rotation": {
"direction": "counter_clock_wise",
"steps": 45,
"duration": 400
}
}
},
"type": "relative_rotary"
},
{
"id": "4d5e6f7a-8b9c-0d1e-2f3a-4b5c6d7e8f9a",
"children": [
{
"rid": "4f317b69-9da0-4b4f-84f2-7ca07b9fe345",
"rtype": "convenience_area_motion"
},
{
"rid": "5f317b69-9da0-4b4f-84f2-7ca07b9fe346",
"rtype": "security_area_motion"
}
],
"services": [
{
"rid": "2b3c4d5e-6f7a-8b9c-0d1e-2f3a4b5c6d7e",
"rtype": "grouped_motion"
},
{
"rid": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"rtype": "grouped_light_level"
}
],
"metadata": {
"name": "Sensor group"
},
"type": "service_group"
},
{
"id": "5e6f7a8b-9c0d-1e2f-3a4b-5c6d7e8f9a0b",
"name": "Test clip resource",
"type": "clip"
},
{
"id": "6f7a8b9c-0d1e-2f3a-4b5c-6d7e8f9a0b1c",
"type": "matter",
"enabled": true,
"max_fabrics": 5,
"has_qr_code": false
},
{
"id": "7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d",
"time": {
"time_zone": "UTC",
"time": "2023-09-23T10:30:00Z"
},
"type": "time"
},
{
"id": "8b9c0d1e-2f3a-4b5c-6d7e-8f9a0b1c2d3e",
"status": "ready",
"type": "zigbee_device_discovery"
}
]
+96 -2
View File
@@ -19,8 +19,7 @@ async def test_binary_sensors(
await setup_platform(hass, mock_bridge_v2, Platform.BINARY_SENSOR)
# there shouldn't have been any requests at this point
assert len(mock_bridge_v2.mock_requests) == 0
# 5 binary_sensors should be created from test data
assert len(hass.states.async_all()) == 5
# 7 binary_sensors should be created from test data
# test motion sensor
sensor = hass.states.get("binary_sensor.hue_motion_sensor_motion")
@@ -81,6 +80,20 @@ async def test_binary_sensors(
assert sensor.name == "Test Camera Motion"
assert sensor.attributes["device_class"] == "motion"
# test grouped motion sensor
sensor = hass.states.get("binary_sensor.sensor_group_motion")
assert sensor is not None
assert sensor.state == "off"
assert sensor.name == "Sensor group Motion"
assert sensor.attributes["device_class"] == "motion"
# test motion aware sensor
sensor = hass.states.get("binary_sensor.motion_aware_sensor_1")
assert sensor is not None
assert sensor.state == "off"
assert sensor.name == "Motion Aware Sensor 1"
assert sensor.attributes["device_class"] == "motion"
async def test_binary_sensor_add_update(
hass: HomeAssistant, mock_bridge_v2: Mock
@@ -110,3 +123,84 @@ async def test_binary_sensor_add_update(
test_entity = hass.states.get(test_entity_id)
assert test_entity is not None
assert test_entity.state == "on"
async def test_grouped_motion_sensor(
hass: HomeAssistant, mock_bridge_v2: Mock, v2_resources_test_data: JsonArrayType
) -> None:
"""Test HueGroupedMotionSensor functionality."""
await mock_bridge_v2.api.load_test_data(v2_resources_test_data)
await setup_platform(hass, mock_bridge_v2, Platform.BINARY_SENSOR)
# test grouped motion sensor exists and has correct state
sensor = hass.states.get("binary_sensor.sensor_group_motion")
assert sensor is not None
assert sensor.state == "off"
assert sensor.attributes["device_class"] == "motion"
# test update of grouped motion sensor works on incoming event
updated_sensor = {
"id": "2b3c4d5e-6f7a-8b9c-0d1e-2f3a4b5c6d7e",
"type": "grouped_motion",
"motion": {
"motion_report": {"changed": "2023-09-23T08:20:51.384Z", "motion": True}
},
}
mock_bridge_v2.api.emit_event("update", updated_sensor)
await hass.async_block_till_done()
sensor = hass.states.get("binary_sensor.sensor_group_motion")
assert sensor.state == "on"
# test disabled grouped motion sensor == state unknown
disabled_sensor = {
"id": "2b3c4d5e-6f7a-8b9c-0d1e-2f3a4b5c6d7e",
"type": "grouped_motion",
"enabled": False,
}
mock_bridge_v2.api.emit_event("update", disabled_sensor)
await hass.async_block_till_done()
sensor = hass.states.get("binary_sensor.sensor_group_motion")
assert sensor.state == "unknown"
async def test_motion_aware_sensor(
hass: HomeAssistant, mock_bridge_v2: Mock, v2_resources_test_data: JsonArrayType
) -> None:
"""Test HueMotionAwareSensor functionality."""
await mock_bridge_v2.api.load_test_data(v2_resources_test_data)
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.motion_aware_sensor_1")
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
updated_sensor = {
"id": "8b7e4f82-9c3d-4e1a-a5f6-8d9c7b2a3e4f",
"type": "security_area_motion",
"motion": {
"motion": True,
"motion_valid": True,
"motion_report": {"changed": "2023-09-23T05:54:08.166Z", "motion": True},
},
}
mock_bridge_v2.api.emit_event("update", updated_sensor)
await hass.async_block_till_done()
sensor = hass.states.get("binary_sensor.motion_aware_sensor_1")
assert sensor.state == "on"
# test name update when motion area configuration name changes
updated_config = {
"id": "5e6f7a8b-9c1d-4e2f-b3a4-5c6d7e8f9a0b",
"type": "motion_area_configuration",
"name": "Updated Motion Area",
}
mock_bridge_v2.api.emit_event("update", updated_config)
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.motion_aware_sensor_1")
assert sensor is not None
assert sensor.name == "Updated Motion Area"
+2 -2
View File
@@ -17,8 +17,8 @@ async def test_event(
"""Test event entity for Hue integration."""
await mock_bridge_v2.api.load_test_data(v2_resources_test_data)
await setup_platform(hass, mock_bridge_v2, Platform.EVENT)
# 7 entities should be created from test data
assert len(hass.states.async_all()) == 7
# 8 entities should be created from test data
assert len(hass.states.async_all()) == 8
# pick one of the remote buttons
state = hass.states.get("event.hue_dimmer_switch_with_4_controls_button_1")
+48 -2
View File
@@ -27,8 +27,8 @@ async def test_sensors(
await setup_platform(hass, mock_bridge_v2, Platform.SENSOR)
# there shouldn't have been any requests at this point
assert len(mock_bridge_v2.mock_requests) == 0
# 6 entities should be created from test data
assert len(hass.states.async_all()) == 6
# 7 entities should be created from test data
assert len(hass.states.async_all()) == 7
# test temperature sensor
sensor = hass.states.get("sensor.hue_motion_sensor_temperature")
@@ -59,6 +59,16 @@ async def test_sensors(
assert sensor.attributes["unit_of_measurement"] == "%"
assert sensor.attributes["battery_state"] == "normal"
# test grouped light level sensor
sensor = hass.states.get("sensor.sensor_group_illuminance")
assert sensor is not None
assert sensor.state == "0"
assert sensor.attributes["friendly_name"] == "Sensor group Illuminance"
assert sensor.attributes["device_class"] == "illuminance"
assert sensor.attributes["state_class"] == "measurement"
assert sensor.attributes["unit_of_measurement"] == "lx"
assert sensor.attributes["light_level"] == 0
# test disabled zigbee_connectivity sensor
entity_id = "sensor.wall_switch_with_2_controls_zigbee_connectivity"
entity_entry = entity_registry.async_get(entity_id)
@@ -139,3 +149,39 @@ async def test_sensor_add_update(hass: HomeAssistant, mock_bridge_v2: Mock) -> N
test_entity = hass.states.get(test_entity_id)
assert test_entity is not None
assert test_entity.state == "22.5"
async def test_grouped_light_level_sensor(
hass: HomeAssistant, mock_bridge_v2: Mock, v2_resources_test_data: JsonArrayType
) -> None:
"""Test HueGroupedLightLevelSensor functionality."""
await mock_bridge_v2.api.load_test_data(v2_resources_test_data)
await setup_platform(hass, mock_bridge_v2, Platform.SENSOR)
# test grouped light level sensor exists and has correct state
sensor = hass.states.get("sensor.sensor_group_illuminance")
assert sensor is not None
assert (
sensor.state == "0"
) # Light level 0 translates to 10^((0-1)/10000) ≈ 0 lux (rounded)
assert sensor.attributes["device_class"] == "illuminance"
assert sensor.attributes["light_level"] == 0
# test update of grouped light level sensor works on incoming event
updated_sensor = {
"id": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"type": "grouped_light_level",
"light": {
"light_level": 30000,
"light_level_report": {
"changed": "2023-09-23T08:20:51.384Z",
"light_level": 30000,
},
},
}
mock_bridge_v2.api.emit_event("update", updated_sensor)
await hass.async_block_till_done()
sensor = hass.states.get("sensor.sensor_group_illuminance")
assert (
sensor.state == "999"
) # Light level 30000 translates to 10^((30000-1)/10000) ≈ 999 lux