mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 12:17:07 +00:00
Adjust Hue integration to use Entity descriptions and translatable entity names (#101413)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
parent
8a83e810b8
commit
6393171fa4
@ -71,6 +71,7 @@ class HueButtonEventEntity(HueBaseEntity, EventEntity):
|
||||
key="button",
|
||||
device_class=EventDeviceClass.BUTTON,
|
||||
translation_key="button",
|
||||
has_entity_name=True,
|
||||
)
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
@ -89,7 +90,8 @@ class HueButtonEventEntity(HueBaseEntity, EventEntity):
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return name for the entity."""
|
||||
return f"{super().name} {self.resource.metadata.control_id}"
|
||||
# this can be translated too as soon as we support arguments into translations ?
|
||||
return f"Button {self.resource.metadata.control_id}"
|
||||
|
||||
@callback
|
||||
def _handle_event(self, event_type: EventType, resource: Button) -> None:
|
||||
@ -112,6 +114,7 @@ class HueRotaryEventEntity(HueBaseEntity, EventEntity):
|
||||
RelativeRotaryDirection.CLOCK_WISE.value,
|
||||
RelativeRotaryDirection.COUNTER_CLOCK_WISE.value,
|
||||
],
|
||||
has_entity_name=True,
|
||||
)
|
||||
|
||||
@callback
|
||||
|
@ -13,7 +13,7 @@ import voluptuous as vol
|
||||
from homeassistant.components.scene import ATTR_TRANSITION, Scene as SceneEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddEntitiesCallback,
|
||||
async_get_current_platform,
|
||||
@ -86,6 +86,8 @@ async def async_setup_entry(
|
||||
class HueSceneEntityBase(HueBaseEntity, SceneEntity):
|
||||
"""Base Representation of a Scene entity from Hue Scenes."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
bridge: HueBridge,
|
||||
@ -97,6 +99,11 @@ class HueSceneEntityBase(HueBaseEntity, SceneEntity):
|
||||
self.resource = resource
|
||||
self.controller = controller
|
||||
self.group = self.controller.get_group(self.resource.id)
|
||||
# we create a virtual service/device for Hue zones/rooms
|
||||
# so we have a parent for grouped lights and scenes
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, self.group.id)},
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Call when entity is added."""
|
||||
@ -112,24 +119,8 @@ class HueSceneEntityBase(HueBaseEntity, SceneEntity):
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return default entity name."""
|
||||
return f"{self.group.metadata.name} {self.resource.metadata.name}"
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return device (service) info."""
|
||||
# we create a virtual service/device for Hue scenes
|
||||
# so we have a parent for grouped lights and scenes
|
||||
group_type = self.group.type.value.title()
|
||||
return DeviceInfo(
|
||||
identifiers={(DOMAIN, self.group.id)},
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
name=self.group.metadata.name,
|
||||
manufacturer=self.bridge.api.config.bridge_device.product_data.manufacturer_name,
|
||||
model=self.group.type.value.title(),
|
||||
suggested_area=self.group.metadata.name if group_type == "Room" else None,
|
||||
via_device=(DOMAIN, self.bridge.api.config.bridge_device.id),
|
||||
)
|
||||
"""Return name of the scene."""
|
||||
return self.resource.metadata.name
|
||||
|
||||
|
||||
class HueSceneEntity(HueSceneEntityBase):
|
||||
|
@ -97,6 +97,7 @@
|
||||
},
|
||||
"sensor": {
|
||||
"zigbee_connectivity": {
|
||||
"name": "Zigbee connectivity",
|
||||
"state": {
|
||||
"connected": "[%key:common::state::connected%]",
|
||||
"disconnected": "[%key:common::state::disconnected%]",
|
||||
@ -106,11 +107,11 @@
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"automation": {
|
||||
"state": {
|
||||
"on": "[%key:common::state::enabled%]",
|
||||
"off": "[%key:common::state::disabled%]"
|
||||
}
|
||||
"motion_sensor_enabled": {
|
||||
"name": "Motion sensor enabled"
|
||||
},
|
||||
"light_sensor_enabled": {
|
||||
"name": "Light sensor enabled"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""Support for switch platform for Hue resources (V2 only)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, TypeAlias
|
||||
from typing import Any
|
||||
|
||||
from aiohue.v2 import HueBridgeV2
|
||||
from aiohue.v2.controllers.config import BehaviorInstance, BehaviorInstanceController
|
||||
@ -27,12 +27,6 @@ from .bridge import HueBridge
|
||||
from .const import DOMAIN
|
||||
from .v2.entity import HueBaseEntity
|
||||
|
||||
ControllerType: TypeAlias = (
|
||||
BehaviorInstanceController | LightLevelController | MotionController
|
||||
)
|
||||
|
||||
SensingService: TypeAlias = LightLevel | Motion
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@ -48,20 +42,22 @@ async def async_setup_entry(
|
||||
raise NotImplementedError("Switch support is only available for V2 bridges")
|
||||
|
||||
@callback
|
||||
def register_items(controller: ControllerType):
|
||||
def register_items(
|
||||
controller: BehaviorInstanceController
|
||||
| LightLevelController
|
||||
| MotionController,
|
||||
switch_class: type[
|
||||
HueBehaviorInstanceEnabledEntity
|
||||
| HueLightSensorEnabledEntity
|
||||
| HueMotionSensorEnabledEntity
|
||||
],
|
||||
):
|
||||
@callback
|
||||
def async_add_entity(
|
||||
event_type: EventType, resource: SensingService | BehaviorInstance
|
||||
event_type: EventType, resource: BehaviorInstance | LightLevel | Motion
|
||||
) -> None:
|
||||
"""Add entity from Hue resource."""
|
||||
if isinstance(resource, BehaviorInstance):
|
||||
async_add_entities(
|
||||
[HueBehaviorInstanceEnabledEntity(bridge, controller, resource)]
|
||||
)
|
||||
else:
|
||||
async_add_entities(
|
||||
[HueSensingServiceEnabledEntity(bridge, controller, resource)]
|
||||
)
|
||||
async_add_entities([switch_class(bridge, api.sensors.motion, resource)])
|
||||
|
||||
# add all current items in controller
|
||||
for item in controller:
|
||||
@ -75,15 +71,23 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
# setup for each switch-type hue resource
|
||||
register_items(api.sensors.motion)
|
||||
register_items(api.sensors.light_level)
|
||||
register_items(api.config.behavior_instance)
|
||||
register_items(api.sensors.motion, HueMotionSensorEnabledEntity)
|
||||
register_items(api.sensors.light_level, HueLightSensorEnabledEntity)
|
||||
register_items(api.config.behavior_instance, HueBehaviorInstanceEnabledEntity)
|
||||
|
||||
|
||||
class HueResourceEnabledEntity(HueBaseEntity, SwitchEntity):
|
||||
"""Representation of a Switch entity from a Hue resource that can be toggled enabled."""
|
||||
|
||||
controller: BehaviorInstanceController | LightLevelController | MotionController
|
||||
resource: BehaviorInstance | LightLevel | Motion
|
||||
|
||||
entity_description = SwitchEntityDescription(
|
||||
key="sensing_service_enabled",
|
||||
device_class=SwitchDeviceClass.SWITCH,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
has_entity_name=True,
|
||||
)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
@ -103,16 +107,6 @@ class HueResourceEnabledEntity(HueBaseEntity, SwitchEntity):
|
||||
)
|
||||
|
||||
|
||||
class HueSensingServiceEnabledEntity(HueResourceEnabledEntity):
|
||||
"""Representation of a Switch entity from Hue SensingService."""
|
||||
|
||||
entity_description = SwitchEntityDescription(
|
||||
key="behavior_instance",
|
||||
device_class=SwitchDeviceClass.SWITCH,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
)
|
||||
|
||||
|
||||
class HueBehaviorInstanceEnabledEntity(HueResourceEnabledEntity):
|
||||
"""Representation of a Switch entity to enable/disable a Hue Behavior Instance."""
|
||||
|
||||
@ -123,10 +117,33 @@ class HueBehaviorInstanceEnabledEntity(HueResourceEnabledEntity):
|
||||
device_class=SwitchDeviceClass.SWITCH,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
has_entity_name=False,
|
||||
translation_key="automation",
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return name for this entity."""
|
||||
return f"Automation: {self.resource.metadata.name}"
|
||||
|
||||
|
||||
class HueMotionSensorEnabledEntity(HueResourceEnabledEntity):
|
||||
"""Representation of a Switch entity to enable/disable a Hue motion sensor."""
|
||||
|
||||
entity_description = SwitchEntityDescription(
|
||||
key="motion_sensor_enabled",
|
||||
device_class=SwitchDeviceClass.SWITCH,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
has_entity_name=True,
|
||||
translation_key="motion_sensor_enabled",
|
||||
)
|
||||
|
||||
|
||||
class HueLightSensorEnabledEntity(HueResourceEnabledEntity):
|
||||
"""Representation of a Switch entity to enable/disable a Hue light sensor."""
|
||||
|
||||
entity_description = SwitchEntityDescription(
|
||||
key="light_sensor_enabled",
|
||||
device_class=SwitchDeviceClass.SWITCH,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
has_entity_name=True,
|
||||
translation_key="light_sensor_enabled",
|
||||
)
|
||||
|
@ -24,8 +24,10 @@ from aiohue.v2.models.tamper import Tamper, TamperState
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
@ -80,25 +82,17 @@ async def async_setup_entry(
|
||||
register_items(api.sensors.tamper, HueTamperSensor)
|
||||
|
||||
|
||||
class HueBinarySensorBase(HueBaseEntity, BinarySensorEntity):
|
||||
"""Representation of a Hue binary_sensor."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
bridge: HueBridge,
|
||||
controller: ControllerType,
|
||||
resource: SensorType,
|
||||
) -> None:
|
||||
"""Initialize the binary sensor."""
|
||||
super().__init__(bridge, controller, resource)
|
||||
self.resource = resource
|
||||
self.controller = controller
|
||||
|
||||
|
||||
class HueMotionSensor(HueBinarySensorBase):
|
||||
class HueMotionSensor(HueBaseEntity, BinarySensorEntity):
|
||||
"""Representation of a Hue Motion sensor."""
|
||||
|
||||
_attr_device_class = BinarySensorDeviceClass.MOTION
|
||||
controller: CameraMotionController | MotionController
|
||||
resource: CameraMotion | Motion
|
||||
|
||||
entity_description = BinarySensorEntityDescription(
|
||||
key="motion_sensor",
|
||||
device_class=BinarySensorDeviceClass.MOTION,
|
||||
has_entity_name=True,
|
||||
)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
@ -109,10 +103,17 @@ class HueMotionSensor(HueBinarySensorBase):
|
||||
return self.resource.motion.value
|
||||
|
||||
|
||||
class HueEntertainmentActiveSensor(HueBinarySensorBase):
|
||||
class HueEntertainmentActiveSensor(HueBaseEntity, BinarySensorEntity):
|
||||
"""Representation of a Hue Entertainment Configuration as binary sensor."""
|
||||
|
||||
_attr_device_class = BinarySensorDeviceClass.RUNNING
|
||||
controller: EntertainmentConfigurationController
|
||||
resource: EntertainmentConfiguration
|
||||
|
||||
entity_description = BinarySensorEntityDescription(
|
||||
key="entertainment_active_sensor",
|
||||
device_class=BinarySensorDeviceClass.RUNNING,
|
||||
has_entity_name=False,
|
||||
)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
@ -122,14 +123,20 @@ class HueEntertainmentActiveSensor(HueBinarySensorBase):
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return sensor name."""
|
||||
type_title = self.resource.type.value.replace("_", " ").title()
|
||||
return f"{self.resource.metadata.name}: {type_title}"
|
||||
return self.resource.metadata.name
|
||||
|
||||
|
||||
class HueContactSensor(HueBinarySensorBase):
|
||||
class HueContactSensor(HueBaseEntity, BinarySensorEntity):
|
||||
"""Representation of a Hue Contact sensor."""
|
||||
|
||||
_attr_device_class = BinarySensorDeviceClass.OPENING
|
||||
controller: ContactController
|
||||
resource: Contact
|
||||
|
||||
entity_description = BinarySensorEntityDescription(
|
||||
key="contact_sensor",
|
||||
device_class=BinarySensorDeviceClass.OPENING,
|
||||
has_entity_name=True,
|
||||
)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
@ -140,10 +147,18 @@ class HueContactSensor(HueBinarySensorBase):
|
||||
return self.resource.contact_report.state != ContactState.CONTACT
|
||||
|
||||
|
||||
class HueTamperSensor(HueBinarySensorBase):
|
||||
class HueTamperSensor(HueBaseEntity, BinarySensorEntity):
|
||||
"""Representation of a Hue Tamper sensor."""
|
||||
|
||||
_attr_device_class = BinarySensorDeviceClass.TAMPER
|
||||
controller: TamperController
|
||||
resource: Tamper
|
||||
|
||||
entity_description = BinarySensorEntityDescription(
|
||||
key="tamper_sensor",
|
||||
device_class=BinarySensorDeviceClass.TAMPER,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
has_entity_name=True,
|
||||
)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
|
@ -3,7 +3,9 @@ from typing import TYPE_CHECKING
|
||||
|
||||
from aiohue.v2 import HueBridgeV2
|
||||
from aiohue.v2.controllers.events import EventType
|
||||
from aiohue.v2.controllers.groups import Room, Zone
|
||||
from aiohue.v2.models.device import Device, DeviceArchetypes
|
||||
from aiohue.v2.models.resource import ResourceTypes
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_CONNECTIONS,
|
||||
@ -33,23 +35,38 @@ async def async_setup_devices(bridge: "HueBridge"):
|
||||
dev_controller = api.devices
|
||||
|
||||
@callback
|
||||
def add_device(hue_device: Device) -> dr.DeviceEntry:
|
||||
def add_device(hue_resource: Device | Room | Zone) -> dr.DeviceEntry:
|
||||
"""Register a Hue device in device registry."""
|
||||
model = f"{hue_device.product_data.product_name} ({hue_device.product_data.model_id})"
|
||||
if isinstance(hue_resource, (Room, Zone)):
|
||||
# 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(),
|
||||
manufacturer=api.config.bridge_device.product_data.manufacturer_name,
|
||||
via_device=(DOMAIN, api.config.bridge_device.id),
|
||||
suggested_area=hue_resource.metadata.name
|
||||
if hue_resource.type == ResourceTypes.ROOM
|
||||
else None,
|
||||
)
|
||||
# Register a Hue device resource as device in HA device registry.
|
||||
model = f"{hue_resource.product_data.product_name} ({hue_resource.product_data.model_id})"
|
||||
params = {
|
||||
ATTR_IDENTIFIERS: {(DOMAIN, hue_device.id)},
|
||||
ATTR_SW_VERSION: hue_device.product_data.software_version,
|
||||
ATTR_NAME: hue_device.metadata.name,
|
||||
ATTR_IDENTIFIERS: {(DOMAIN, hue_resource.id)},
|
||||
ATTR_SW_VERSION: hue_resource.product_data.software_version,
|
||||
ATTR_NAME: hue_resource.metadata.name,
|
||||
ATTR_MODEL: model,
|
||||
ATTR_MANUFACTURER: hue_device.product_data.manufacturer_name,
|
||||
ATTR_MANUFACTURER: hue_resource.product_data.manufacturer_name,
|
||||
}
|
||||
if room := dev_controller.get_room(hue_device.id):
|
||||
if room := dev_controller.get_room(hue_resource.id):
|
||||
params[ATTR_SUGGESTED_AREA] = room.metadata.name
|
||||
if hue_device.metadata.archetype == DeviceArchetypes.BRIDGE_V2:
|
||||
if hue_resource.metadata.archetype == DeviceArchetypes.BRIDGE_V2:
|
||||
params[ATTR_IDENTIFIERS].add((DOMAIN, api.config.bridge_id))
|
||||
else:
|
||||
params[ATTR_VIA_DEVICE] = (DOMAIN, api.config.bridge_device.id)
|
||||
zigbee = dev_controller.get_zigbee_connectivity(hue_device.id)
|
||||
zigbee = dev_controller.get_zigbee_connectivity(hue_resource.id)
|
||||
if zigbee and zigbee.mac_address:
|
||||
params[ATTR_CONNECTIONS] = {(dr.CONNECTION_NETWORK_MAC, zigbee.mac_address)}
|
||||
|
||||
@ -63,25 +80,27 @@ async def async_setup_devices(bridge: "HueBridge"):
|
||||
dev_reg.async_remove_device(device.id)
|
||||
|
||||
@callback
|
||||
def handle_device_event(evt_type: EventType, hue_device: Device) -> None:
|
||||
"""Handle event from Hue devices controller."""
|
||||
def handle_device_event(
|
||||
evt_type: EventType, hue_resource: Device | Room | Zone
|
||||
) -> None:
|
||||
"""Handle event from Hue controller."""
|
||||
if evt_type == EventType.RESOURCE_DELETED:
|
||||
remove_device(hue_device.id)
|
||||
remove_device(hue_resource.id)
|
||||
else:
|
||||
# updates to existing device will also be handled by this call
|
||||
add_device(hue_device)
|
||||
add_device(hue_resource)
|
||||
|
||||
# create/update all current devices found in controller
|
||||
# create/update all current devices found in controllers
|
||||
known_devices = [add_device(hue_device) for hue_device in dev_controller]
|
||||
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]
|
||||
|
||||
# Check for nodes that no longer exist and remove them
|
||||
for device in dr.async_entries_for_config_entry(dev_reg, entry.entry_id):
|
||||
if device not in known_devices:
|
||||
# handle case where a virtual device was created for a Hue group
|
||||
hue_dev_id = next(x[1] for x in device.identifiers if x[0] == DOMAIN)
|
||||
if hue_dev_id in api.groups:
|
||||
continue
|
||||
dev_reg.async_remove_device(device.id)
|
||||
|
||||
# add listener for updates on Hue devices controller
|
||||
# add listener for updates on Hue controllers
|
||||
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))
|
||||
|
@ -9,10 +9,7 @@ from aiohue.v2.models.resource import ResourceTypes
|
||||
from aiohue.v2.models.zigbee_connectivity import ConnectivityServiceStatus
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.device_registry import (
|
||||
DeviceInfo,
|
||||
async_get as async_get_device_registry,
|
||||
)
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_registry import async_get as async_get_entity_registry
|
||||
|
||||
@ -72,24 +69,6 @@ class HueBaseEntity(Entity):
|
||||
self._ignore_availability = None
|
||||
self._last_state = None
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return name for the entity."""
|
||||
if self.device is None:
|
||||
# this is just a guard
|
||||
# creating a pretty name for device-less entities (e.g. groups/scenes)
|
||||
# should be handled in the platform instead
|
||||
return self.resource.type.value
|
||||
dev_name = self.device.metadata.name
|
||||
# if resource is a light, use the device name itself
|
||||
if self.resource.type == ResourceTypes.LIGHT:
|
||||
return dev_name
|
||||
# for sensors etc, use devicename + pretty name of type
|
||||
type_title = RESOURCE_TYPE_NAMES.get(
|
||||
self.resource.type, self.resource.type.value.replace("_", " ").title()
|
||||
)
|
||||
return f"{dev_name} {type_title}"
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Call when entity is added."""
|
||||
self._check_availability()
|
||||
@ -146,19 +125,12 @@ class HueBaseEntity(Entity):
|
||||
def _handle_event(self, event_type: EventType, resource: HueResource) -> None:
|
||||
"""Handle status event for this resource (or it's parent)."""
|
||||
if event_type == EventType.RESOURCE_DELETED:
|
||||
# handle removal of room and zone 'virtual' devices/services
|
||||
# regular devices are removed automatically by the logic in device.py.
|
||||
if resource.type in (ResourceTypes.ROOM, ResourceTypes.ZONE):
|
||||
dev_reg = async_get_device_registry(self.hass)
|
||||
if device := dev_reg.async_get_device(
|
||||
identifiers={(DOMAIN, resource.id)}
|
||||
):
|
||||
dev_reg.async_remove_device(device.id)
|
||||
# cleanup entities that are not strictly device-bound and have the bridge as parent
|
||||
if self.device is None:
|
||||
if self.device is None and resource.id == self.resource.id:
|
||||
ent_reg = async_get_entity_registry(self.hass)
|
||||
ent_reg.async_remove(self.entity_id)
|
||||
return
|
||||
|
||||
self.logger.debug("Received status update for %s", self.entity_id)
|
||||
self._check_availability()
|
||||
self.on_update()
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""Support for Hue groups (room/zone)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
from aiohue.v2 import HueBridgeV2
|
||||
@ -17,11 +18,12 @@ from homeassistant.components.light import (
|
||||
FLASH_SHORT,
|
||||
ColorMode,
|
||||
LightEntity,
|
||||
LightEntityDescription,
|
||||
LightEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from ..bridge import HueBridge
|
||||
@ -43,18 +45,26 @@ async def async_setup_entry(
|
||||
bridge: HueBridge = hass.data[DOMAIN][config_entry.entry_id]
|
||||
api: HueBridgeV2 = bridge.api
|
||||
|
||||
@callback
|
||||
def async_add_light(event_type: EventType, resource: GroupedLight) -> None:
|
||||
async def async_add_light(event_type: EventType, resource: GroupedLight) -> None:
|
||||
"""Add Grouped Light for Hue Room/Zone."""
|
||||
group = api.groups.grouped_light.get_zone(resource.id)
|
||||
# delay group creation a bit due to a race condition where the
|
||||
# grouped_light resource is created before the zone/room
|
||||
retries = 5
|
||||
while (
|
||||
retries
|
||||
and (group := api.groups.grouped_light.get_zone(resource.id)) is None
|
||||
):
|
||||
retries -= 1
|
||||
await asyncio.sleep(0.5)
|
||||
if group is None:
|
||||
# guard, just in case
|
||||
return
|
||||
light = GroupedHueLight(bridge, resource, group)
|
||||
async_add_entities([light])
|
||||
|
||||
# add current items
|
||||
for item in api.groups.grouped_light.items:
|
||||
async_add_light(EventType.RESOURCE_ADDED, item)
|
||||
await async_add_light(EventType.RESOURCE_ADDED, item)
|
||||
|
||||
# register listener for new grouped_light
|
||||
config_entry.async_on_unload(
|
||||
@ -67,7 +77,12 @@ async def async_setup_entry(
|
||||
class GroupedHueLight(HueBaseEntity, LightEntity):
|
||||
"""Representation of a Grouped Hue light."""
|
||||
|
||||
_attr_icon = "mdi:lightbulb-group"
|
||||
entity_description = LightEntityDescription(
|
||||
key="hue_grouped_light",
|
||||
icon="mdi:lightbulb-group",
|
||||
has_entity_name=True,
|
||||
name=None,
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self, bridge: HueBridge, resource: GroupedLight, group: Room | Zone
|
||||
@ -81,7 +96,11 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
|
||||
self.api: HueBridgeV2 = bridge.api
|
||||
self._attr_supported_features |= LightEntityFeature.FLASH
|
||||
self._attr_supported_features |= LightEntityFeature.TRANSITION
|
||||
|
||||
# we create a virtual service/device for Hue zones/rooms
|
||||
# so we have a parent for grouped lights and scenes
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, self.group.id)},
|
||||
)
|
||||
self._dynamic_mode_active = False
|
||||
self._update_values()
|
||||
|
||||
@ -103,11 +122,6 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
|
||||
self.api.lights.subscribe(self._handle_event, light_ids)
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return name of room/zone for this grouped light."""
|
||||
return self.group.metadata.name
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if light is on."""
|
||||
@ -131,22 +145,6 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
|
||||
"dynamics": self._dynamic_mode_active,
|
||||
}
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return device (service) info."""
|
||||
# we create a virtual service/device for Hue zones/rooms
|
||||
# so we have a parent for grouped lights and scenes
|
||||
model = self.group.type.value.title()
|
||||
return DeviceInfo(
|
||||
identifiers={(DOMAIN, self.group.id)},
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
name=self.group.metadata.name,
|
||||
manufacturer=self.api.config.bridge_device.product_data.manufacturer_name,
|
||||
model=model,
|
||||
suggested_area=self.group.metadata.name if model == "Room" else None,
|
||||
via_device=(DOMAIN, self.api.config.bridge_device.id),
|
||||
)
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the grouped_light on."""
|
||||
transition = normalize_hue_transition(kwargs.get(ATTR_TRANSITION))
|
||||
|
@ -19,6 +19,7 @@ from homeassistant.components.light import (
|
||||
FLASH_SHORT,
|
||||
ColorMode,
|
||||
LightEntity,
|
||||
LightEntityDescription,
|
||||
LightEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@ -69,6 +70,10 @@ async def async_setup_entry(
|
||||
class HueLight(HueBaseEntity, LightEntity):
|
||||
"""Representation of a Hue light."""
|
||||
|
||||
entity_description = LightEntityDescription(
|
||||
key="hue_light", has_entity_name=True, name=None
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self, bridge: HueBridge, controller: LightsController, resource: Light
|
||||
) -> None:
|
||||
|
@ -20,6 +20,7 @@ from aiohue.v2.models.zigbee_connectivity import ZigbeeConnectivity
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@ -93,9 +94,13 @@ class HueSensorBase(HueBaseEntity, SensorEntity):
|
||||
class HueTemperatureSensor(HueSensorBase):
|
||||
"""Representation of a Hue Temperature sensor."""
|
||||
|
||||
_attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS
|
||||
_attr_device_class = SensorDeviceClass.TEMPERATURE
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
entity_description = SensorEntityDescription(
|
||||
key="temperature_sensor",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
has_entity_name=True,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> float:
|
||||
@ -106,9 +111,13 @@ class HueTemperatureSensor(HueSensorBase):
|
||||
class HueLightLevelSensor(HueSensorBase):
|
||||
"""Representation of a Hue LightLevel (illuminance) sensor."""
|
||||
|
||||
_attr_native_unit_of_measurement = LIGHT_LUX
|
||||
_attr_device_class = SensorDeviceClass.ILLUMINANCE
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
entity_description = SensorEntityDescription(
|
||||
key="lightlevel_sensor",
|
||||
device_class=SensorDeviceClass.ILLUMINANCE,
|
||||
native_unit_of_measurement=LIGHT_LUX,
|
||||
has_entity_name=True,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> int:
|
||||
@ -130,10 +139,14 @@ class HueLightLevelSensor(HueSensorBase):
|
||||
class HueBatterySensor(HueSensorBase):
|
||||
"""Representation of a Hue Battery sensor."""
|
||||
|
||||
_attr_native_unit_of_measurement = PERCENTAGE
|
||||
_attr_device_class = SensorDeviceClass.BATTERY
|
||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
entity_description = SensorEntityDescription(
|
||||
key="battery_sensor",
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
has_entity_name=True,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> int:
|
||||
@ -151,16 +164,20 @@ class HueBatterySensor(HueSensorBase):
|
||||
class HueZigbeeConnectivitySensor(HueSensorBase):
|
||||
"""Representation of a Hue ZigbeeConnectivity sensor."""
|
||||
|
||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||
_attr_translation_key = "zigbee_connectivity"
|
||||
_attr_device_class = SensorDeviceClass.ENUM
|
||||
_attr_options = [
|
||||
"connected",
|
||||
"disconnected",
|
||||
"connectivity_issue",
|
||||
"unidirectional_incoming",
|
||||
]
|
||||
_attr_entity_registry_enabled_default = False
|
||||
entity_description = SensorEntityDescription(
|
||||
key="zigbee_connectivity_sensor",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
has_entity_name=True,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
translation_key="zigbee_connectivity",
|
||||
options=[
|
||||
"connected",
|
||||
"disconnected",
|
||||
"connectivity_issue",
|
||||
"unidirectional_incoming",
|
||||
],
|
||||
entity_registry_enabled_default=False,
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> str:
|
||||
|
@ -3,6 +3,7 @@ import asyncio
|
||||
from collections import deque
|
||||
import json
|
||||
import logging
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
import aiohue.v1 as aiohue_v1
|
||||
@ -12,6 +13,7 @@ import pytest
|
||||
|
||||
from homeassistant.components import hue
|
||||
from homeassistant.components.hue.v1 import sensor_base as hue_sensor_base
|
||||
from homeassistant.components.hue.v2.device import async_setup_devices
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import (
|
||||
@ -20,6 +22,7 @@ from tests.common import (
|
||||
load_fixture,
|
||||
mock_device_registry,
|
||||
)
|
||||
from tests.components.hue.const import FAKE_BRIDGE, FAKE_BRIDGE_DEVICE
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
@ -56,6 +59,8 @@ def create_mock_bridge(hass, api_version=1):
|
||||
async def async_initialize_bridge():
|
||||
if bridge.config_entry:
|
||||
hass.data.setdefault(hue.DOMAIN, {})[bridge.config_entry.entry_id] = bridge
|
||||
if bridge.api_version == 2:
|
||||
await async_setup_devices(bridge)
|
||||
return True
|
||||
|
||||
bridge.async_initialize_bridge = async_initialize_bridge
|
||||
@ -140,22 +145,10 @@ def create_mock_api_v2(hass):
|
||||
"""Create a mock V2 API."""
|
||||
api = Mock(spec=aiohue_v2.HueBridgeV2)
|
||||
api.initialize = AsyncMock()
|
||||
api.config = Mock(
|
||||
bridge_id="aabbccddeeffggh",
|
||||
mac_address="00:17:88:01:aa:bb:fd:c7",
|
||||
model_id="BSB002",
|
||||
api_version="9.9.9",
|
||||
software_version="1935144040",
|
||||
bridge_device=Mock(
|
||||
id="4a507550-8742-4087-8bf5-c2334f29891c",
|
||||
product_data=Mock(manufacturer_name="Mock"),
|
||||
),
|
||||
spec=aiohue_v2.ConfigController,
|
||||
)
|
||||
api.config.name = "Home"
|
||||
api.mock_requests = []
|
||||
|
||||
api.logger = logging.getLogger(__name__)
|
||||
api.config = aiohue_v2.ConfigController(api)
|
||||
api.events = aiohue_v2.EventStream(api)
|
||||
api.devices = aiohue_v2.DevicesController(api)
|
||||
api.lights = aiohue_v2.LightsController(api)
|
||||
@ -171,9 +164,13 @@ def create_mock_api_v2(hass):
|
||||
|
||||
api.request = mock_request
|
||||
|
||||
async def load_test_data(data):
|
||||
async def load_test_data(data: list[dict[str, Any]]):
|
||||
"""Load test data into controllers."""
|
||||
api.config = aiohue_v2.ConfigController(api)
|
||||
|
||||
# append default bridge if none explicitly given in test data
|
||||
if not any(x for x in data if x["type"] == "bridge"):
|
||||
data.append(FAKE_BRIDGE)
|
||||
data.append(FAKE_BRIDGE_DEVICE)
|
||||
|
||||
await asyncio.gather(
|
||||
api.config.initialize(data),
|
||||
|
@ -1,5 +1,29 @@
|
||||
"""Constants for Hue tests."""
|
||||
|
||||
FAKE_BRIDGE = {
|
||||
"bridge_id": "aabbccddeeffggh",
|
||||
"id": "07dd5849-abcd-efgh-b9b9-eb540408ce00",
|
||||
"id_v1": "",
|
||||
"owner": {"rid": "4a507550-8742-4087-8bf5-c2334f29891c", "rtype": "device"},
|
||||
"time_zone": {"time_zone": "Europe/Amsterdam"},
|
||||
"type": "bridge",
|
||||
}
|
||||
|
||||
FAKE_BRIDGE_DEVICE = {
|
||||
"id": "4a507550-8742-4087-8bf5-c2334f29891c",
|
||||
"id_v1": "",
|
||||
"metadata": {"archetype": "bridge_v2", "name": "Philips hue"},
|
||||
"product_data": {
|
||||
"certified": True,
|
||||
"manufacturer_name": "Signify Netherlands B.V.",
|
||||
"model_id": "BSB002",
|
||||
"product_archetype": "bridge_v2",
|
||||
"product_name": "Philips hue",
|
||||
"software_version": "1.50.1950111030",
|
||||
},
|
||||
"services": [{"rid": "07dd5849-abcd-efgh-b9b9-eb540408ce00", "rtype": "bridge"}],
|
||||
"type": "device",
|
||||
}
|
||||
|
||||
FAKE_DEVICE = {
|
||||
"id": "fake_device_id_1",
|
||||
|
@ -25,19 +25,17 @@ async def test_binary_sensors(
|
||||
assert sensor.attributes["device_class"] == "motion"
|
||||
|
||||
# test entertainment room active sensor
|
||||
sensor = hass.states.get(
|
||||
"binary_sensor.entertainmentroom_1_entertainment_configuration"
|
||||
)
|
||||
sensor = hass.states.get("binary_sensor.entertainmentroom_1")
|
||||
assert sensor is not None
|
||||
assert sensor.state == "off"
|
||||
assert sensor.name == "Entertainmentroom 1: Entertainment Configuration"
|
||||
assert sensor.name == "Entertainmentroom 1"
|
||||
assert sensor.attributes["device_class"] == "running"
|
||||
|
||||
# test contact sensor
|
||||
sensor = hass.states.get("binary_sensor.test_contact_sensor_contact")
|
||||
sensor = hass.states.get("binary_sensor.test_contact_sensor_opening")
|
||||
assert sensor is not None
|
||||
assert sensor.state == "off"
|
||||
assert sensor.name == "Test contact sensor Contact"
|
||||
assert sensor.name == "Test contact sensor Opening"
|
||||
assert sensor.attributes["device_class"] == "opening"
|
||||
# test contact sensor disabled == state unknown
|
||||
mock_bridge_v2.api.emit_event(
|
||||
@ -49,7 +47,7 @@ async def test_binary_sensors(
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
sensor = hass.states.get("binary_sensor.test_contact_sensor_contact")
|
||||
sensor = hass.states.get("binary_sensor.test_contact_sensor_opening")
|
||||
assert sensor.state == "unknown"
|
||||
|
||||
# test tamper sensor
|
||||
|
@ -57,7 +57,7 @@ async def test_sensor_add_update(hass: HomeAssistant, mock_bridge_v2) -> None:
|
||||
await mock_bridge_v2.api.load_test_data([FAKE_DEVICE, FAKE_ZIGBEE_CONNECTIVITY])
|
||||
await setup_platform(hass, mock_bridge_v2, "event")
|
||||
|
||||
test_entity_id = "event.hue_mocked_device_relative_rotary"
|
||||
test_entity_id = "event.hue_mocked_device_rotary"
|
||||
|
||||
# verify entity does not exist before we start
|
||||
assert hass.states.get(test_entity_id) is None
|
||||
@ -70,7 +70,7 @@ async def test_sensor_add_update(hass: HomeAssistant, mock_bridge_v2) -> None:
|
||||
state = hass.states.get(test_entity_id)
|
||||
assert state is not None
|
||||
assert state.state == "unknown"
|
||||
assert state.name == "Hue mocked device Relative Rotary"
|
||||
assert state.name == "Hue mocked device Rotary"
|
||||
# check event_types
|
||||
assert state.attributes[ATTR_EVENT_TYPES] == ["clock_wise", "counter_clock_wise"]
|
||||
|
||||
|
@ -186,7 +186,7 @@ async def test_scene_updates(
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
test_entity = hass.states.get(test_entity_id)
|
||||
assert test_entity.name == "Test Room 2 Mocked Scene"
|
||||
assert test_entity.attributes["group_name"] == "Test Room 2"
|
||||
|
||||
# # test delete
|
||||
mock_bridge_v2.api.emit_event("delete", updated_resource)
|
||||
|
@ -18,9 +18,9 @@ async def test_switch(
|
||||
assert len(hass.states.async_all()) == 4
|
||||
|
||||
# test config switch to enable/disable motion sensor
|
||||
test_entity = hass.states.get("switch.hue_motion_sensor_motion")
|
||||
test_entity = hass.states.get("switch.hue_motion_sensor_motion_sensor_enabled")
|
||||
assert test_entity is not None
|
||||
assert test_entity.name == "Hue motion sensor Motion"
|
||||
assert test_entity.name == "Hue motion sensor Motion sensor enabled"
|
||||
assert test_entity.state == "on"
|
||||
assert test_entity.attributes["device_class"] == "switch"
|
||||
|
||||
@ -40,7 +40,7 @@ async def test_switch_turn_on_service(
|
||||
|
||||
await setup_platform(hass, mock_bridge_v2, "switch")
|
||||
|
||||
test_entity_id = "switch.hue_motion_sensor_motion"
|
||||
test_entity_id = "switch.hue_motion_sensor_motion_sensor_enabled"
|
||||
|
||||
# call the HA turn_on service
|
||||
await hass.services.async_call(
|
||||
@ -64,7 +64,7 @@ async def test_switch_turn_off_service(
|
||||
|
||||
await setup_platform(hass, mock_bridge_v2, "switch")
|
||||
|
||||
test_entity_id = "switch.hue_motion_sensor_motion"
|
||||
test_entity_id = "switch.hue_motion_sensor_motion_sensor_enabled"
|
||||
|
||||
# verify the switch is on before we start
|
||||
assert hass.states.get(test_entity_id).state == "on"
|
||||
@ -103,7 +103,7 @@ async def test_switch_added(hass: HomeAssistant, mock_bridge_v2) -> None:
|
||||
|
||||
await setup_platform(hass, mock_bridge_v2, "switch")
|
||||
|
||||
test_entity_id = "switch.hue_mocked_device_motion"
|
||||
test_entity_id = "switch.hue_mocked_device_motion_sensor_enabled"
|
||||
|
||||
# verify entity does not exist before we start
|
||||
assert hass.states.get(test_entity_id) is None
|
||||
|
Loading…
x
Reference in New Issue
Block a user