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:
committed by
GitHub
parent
60bf298ca6
commit
2008a73657
@@ -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."""
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user