diff --git a/homeassistant/components/hue/event.py b/homeassistant/components/hue/event.py index 914067509b7..da59515e7be 100644 --- a/homeassistant/components/hue/event.py +++ b/homeassistant/components/hue/event.py @@ -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 diff --git a/homeassistant/components/hue/scene.py b/homeassistant/components/hue/scene.py index bd290d0bbb8..17f7a81b2a5 100644 --- a/homeassistant/components/hue/scene.py +++ b/homeassistant/components/hue/scene.py @@ -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): diff --git a/homeassistant/components/hue/strings.json b/homeassistant/components/hue/strings.json index 1af6d3b58b5..4022c61bc36 100644 --- a/homeassistant/components/hue/strings.json +++ b/homeassistant/components/hue/strings.json @@ -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" } } }, diff --git a/homeassistant/components/hue/switch.py b/homeassistant/components/hue/switch.py index 0fb2ebd6b52..c9da30a779c 100644 --- a/homeassistant/components/hue/switch.py +++ b/homeassistant/components/hue/switch.py @@ -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", + ) diff --git a/homeassistant/components/hue/v2/binary_sensor.py b/homeassistant/components/hue/v2/binary_sensor.py index 1eded0429b8..f1bcd0bbbe3 100644 --- a/homeassistant/components/hue/v2/binary_sensor.py +++ b/homeassistant/components/hue/v2/binary_sensor.py @@ -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: diff --git a/homeassistant/components/hue/v2/device.py b/homeassistant/components/hue/v2/device.py index 6fed4bc16d1..75f474cc0ea 100644 --- a/homeassistant/components/hue/v2/device.py +++ b/homeassistant/components/hue/v2/device.py @@ -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)) diff --git a/homeassistant/components/hue/v2/entity.py b/homeassistant/components/hue/v2/entity.py index f4c76618009..75e4bb1edd4 100644 --- a/homeassistant/components/hue/v2/entity.py +++ b/homeassistant/components/hue/v2/entity.py @@ -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() diff --git a/homeassistant/components/hue/v2/group.py b/homeassistant/components/hue/v2/group.py index 9985d37627b..7d63df131d8 100644 --- a/homeassistant/components/hue/v2/group.py +++ b/homeassistant/components/hue/v2/group.py @@ -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)) diff --git a/homeassistant/components/hue/v2/light.py b/homeassistant/components/hue/v2/light.py index f42da406599..ed5d0151b03 100644 --- a/homeassistant/components/hue/v2/light.py +++ b/homeassistant/components/hue/v2/light.py @@ -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: diff --git a/homeassistant/components/hue/v2/sensor.py b/homeassistant/components/hue/v2/sensor.py index 4bfb727b917..56f708e2dfd 100644 --- a/homeassistant/components/hue/v2/sensor.py +++ b/homeassistant/components/hue/v2/sensor.py @@ -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: diff --git a/tests/components/hue/conftest.py b/tests/components/hue/conftest.py index d730d3f18f5..3350ea15185 100644 --- a/tests/components/hue/conftest.py +++ b/tests/components/hue/conftest.py @@ -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), diff --git a/tests/components/hue/const.py b/tests/components/hue/const.py index 415fe1324b7..252c9da9a9d 100644 --- a/tests/components/hue/const.py +++ b/tests/components/hue/const.py @@ -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", diff --git a/tests/components/hue/test_binary_sensor.py b/tests/components/hue/test_binary_sensor.py index 3846f17aa76..ab6f4ab0581 100644 --- a/tests/components/hue/test_binary_sensor.py +++ b/tests/components/hue/test_binary_sensor.py @@ -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 diff --git a/tests/components/hue/test_event.py b/tests/components/hue/test_event.py index a3779c6b0e3..9953bb11796 100644 --- a/tests/components/hue/test_event.py +++ b/tests/components/hue/test_event.py @@ -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"] diff --git a/tests/components/hue/test_scene.py b/tests/components/hue/test_scene.py index 7785b9d4628..5fa35cec5b4 100644 --- a/tests/components/hue/test_scene.py +++ b/tests/components/hue/test_scene.py @@ -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) diff --git a/tests/components/hue/test_switch.py b/tests/components/hue/test_switch.py index e8cad2bc802..c3384ae1e44 100644 --- a/tests/components/hue/test_switch.py +++ b/tests/components/hue/test_switch.py @@ -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