1
mirror of https://github.com/home-assistant/core synced 2024-08-28 03:36:46 +02: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:
Marcel van der Veldt 2023-10-09 14:14:07 +02:00 committed by GitHub
parent 8a83e810b8
commit 6393171fa4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 266 additions and 209 deletions

View File

@ -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

View File

@ -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):

View File

@ -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"
}
}
},

View File

@ -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",
)

View File

@ -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:

View File

@ -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))

View File

@ -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()

View File

@ -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))

View File

@ -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:

View File

@ -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:

View File

@ -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),

View File

@ -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",

View File

@ -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

View File

@ -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"]

View File

@ -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)

View File

@ -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