Add virtual device/service for Hue groups (#68569)
This commit is contained in:
parent
7e8d52e5a3
commit
09f6785956
5 changed files with 75 additions and 67 deletions
|
@ -16,6 +16,8 @@ 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 import entity_platform
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
|
||||
from .bridge import HueBridge
|
||||
from .const import DOMAIN
|
||||
|
@ -106,8 +108,7 @@ class HueSceneEntity(HueBaseEntity, SceneEntity):
|
|||
@property
|
||||
def name(self) -> str:
|
||||
"""Return default entity name."""
|
||||
group = self.controller.get_group(self.resource.id)
|
||||
return f"{group.metadata.name} - {self.resource.metadata.name}"
|
||||
return f"{self.group.metadata.name} {self.resource.metadata.name}"
|
||||
|
||||
@property
|
||||
def is_dynamic(self) -> bool:
|
||||
|
@ -167,3 +168,18 @@ class HueSceneEntity(HueBaseEntity, SceneEntity):
|
|||
"brightness": brightness,
|
||||
"is_dynamic": self.is_dynamic,
|
||||
}
|
||||
|
||||
@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
|
||||
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,
|
||||
via_device=(DOMAIN, self.bridge.api.config.bridge_device.id),
|
||||
)
|
||||
|
|
|
@ -9,6 +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 async_get as async_get_device_registry
|
||||
from homeassistant.helpers.entity import DeviceInfo, Entity
|
||||
from homeassistant.helpers.entity_registry import async_get as async_get_entity_registry
|
||||
|
||||
|
@ -135,17 +136,21 @@ class HueBaseEntity(Entity):
|
|||
@callback
|
||||
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 and resource.id == self.resource.id:
|
||||
self.logger.debug("Received delete for %s", self.entity_id)
|
||||
# non-device bound entities like groups and scenes need to be removed here
|
||||
# all others will be be removed by device setup in case of device removal
|
||||
ent_reg = async_get_entity_registry(self.hass)
|
||||
ent_reg.async_remove(self.entity_id)
|
||||
else:
|
||||
self.logger.debug("Received status update for %s", self.entity_id)
|
||||
self._check_availability()
|
||||
self.on_update()
|
||||
self.async_write_ha_state()
|
||||
if event_type == EventType.RESOURCE_DELETED:
|
||||
# remove any services created for zones/rooms
|
||||
# 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({(DOMAIN, resource.id)}):
|
||||
dev_reg.async_remove_device(device.id)
|
||||
if resource.type in [ResourceTypes.GROUPED_LIGHT, ResourceTypes.SCENE]:
|
||||
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()
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def _check_availability(self):
|
||||
|
|
|
@ -25,10 +25,12 @@ from homeassistant.components.light import (
|
|||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from ..bridge import HueBridge
|
||||
from ..const import CONF_ALLOW_HUE_GROUPS, DOMAIN
|
||||
from ..const import DOMAIN
|
||||
from .entity import HueBaseEntity
|
||||
from .helpers import (
|
||||
normalize_hue_brightness,
|
||||
|
@ -46,30 +48,22 @@ async def async_setup_entry(
|
|||
bridge: HueBridge = hass.data[DOMAIN][config_entry.entry_id]
|
||||
api: HueBridgeV2 = bridge.api
|
||||
|
||||
# to prevent race conditions (groupedlight is created before zone/room)
|
||||
# we create groupedlights from the room/zone and actually use the
|
||||
# underlying grouped_light resource for control
|
||||
|
||||
@callback
|
||||
def async_add_light(event_type: EventType, resource: Room | Zone) -> None:
|
||||
def async_add_light(event_type: EventType, resource: GroupedLight) -> None:
|
||||
"""Add Grouped Light for Hue Room/Zone."""
|
||||
if grouped_light_id := resource.grouped_light:
|
||||
grouped_light = api.groups.grouped_light[grouped_light_id]
|
||||
light = GroupedHueLight(bridge, grouped_light, resource)
|
||||
async_add_entities([light])
|
||||
group = api.groups.grouped_light.get_zone(resource.id)
|
||||
if group is None:
|
||||
return
|
||||
light = GroupedHueLight(bridge, resource, group)
|
||||
async_add_entities([light])
|
||||
|
||||
# add current items
|
||||
for item in api.groups.room.items + api.groups.zone.items:
|
||||
for item in api.groups.grouped_light.items:
|
||||
async_add_light(EventType.RESOURCE_ADDED, item)
|
||||
|
||||
# register listener for new zones/rooms
|
||||
# register listener for new grouped_light
|
||||
config_entry.async_on_unload(
|
||||
api.groups.room.subscribe(
|
||||
async_add_light, event_filter=EventType.RESOURCE_ADDED
|
||||
)
|
||||
)
|
||||
config_entry.async_on_unload(
|
||||
api.groups.zone.subscribe(
|
||||
api.groups.grouped_light.subscribe(
|
||||
async_add_light, event_filter=EventType.RESOURCE_ADDED
|
||||
)
|
||||
)
|
||||
|
@ -93,11 +87,6 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
|
|||
self._attr_supported_features |= SUPPORT_FLASH
|
||||
self._attr_supported_features |= SUPPORT_TRANSITION
|
||||
|
||||
# Entities for Hue groups are disabled by default
|
||||
# unless they were enabled in old version (legacy option)
|
||||
self._attr_entity_registry_enabled_default = bridge.config_entry.options.get(
|
||||
CONF_ALLOW_HUE_GROUPS, False
|
||||
)
|
||||
self._dynamic_mode_active = False
|
||||
self._update_values()
|
||||
|
||||
|
@ -144,6 +133,22 @@ 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))
|
||||
|
|
|
@ -14,8 +14,8 @@ async def test_lights(hass, mock_bridge_v2, v2_resources_test_data):
|
|||
await setup_platform(hass, mock_bridge_v2, "light")
|
||||
# there shouldn't have been any requests at this point
|
||||
assert len(mock_bridge_v2.mock_requests) == 0
|
||||
# 6 entities should be created from test data (grouped_lights are disabled by default)
|
||||
assert len(hass.states.async_all()) == 6
|
||||
# 8 entities should be created from test data
|
||||
assert len(hass.states.async_all()) == 8
|
||||
|
||||
# test light which supports color and color temperature
|
||||
light_1 = hass.states.get("light.hue_light_with_color_and_color_temperature_1")
|
||||
|
@ -329,32 +329,14 @@ async def test_grouped_lights(hass, mock_bridge_v2, v2_resources_test_data):
|
|||
|
||||
await setup_platform(hass, mock_bridge_v2, "light")
|
||||
|
||||
# test if entities for hue groups are created and disabled by default
|
||||
# test if entities for hue groups are created and enabled by default
|
||||
for entity_id in ("light.test_zone", "light.test_room"):
|
||||
ent_reg = er.async_get(hass)
|
||||
entity_entry = ent_reg.async_get(entity_id)
|
||||
|
||||
assert entity_entry
|
||||
assert entity_entry.disabled
|
||||
assert entity_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION
|
||||
# entity should not have a device assigned
|
||||
assert entity_entry.device_id is None
|
||||
|
||||
# enable the entity
|
||||
updated_entry = ent_reg.async_update_entity(
|
||||
entity_entry.entity_id, **{"disabled_by": None}
|
||||
)
|
||||
assert updated_entry != entity_entry
|
||||
assert updated_entry.disabled is False
|
||||
|
||||
# reload platform and check if entities are correctly there
|
||||
await hass.config_entries.async_forward_entry_unload(
|
||||
mock_bridge_v2.config_entry, "light"
|
||||
)
|
||||
await hass.config_entries.async_forward_entry_setup(
|
||||
mock_bridge_v2.config_entry, "light"
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
# scene entities should have be assigned to the room/zone device/service
|
||||
assert entity_entry.device_id is not None
|
||||
|
||||
# test light created for hue zone
|
||||
test_entity = hass.states.get("light.test_zone")
|
||||
|
|
|
@ -21,7 +21,7 @@ async def test_scene(hass, mock_bridge_v2, v2_resources_test_data):
|
|||
# test (dynamic) scene for a hue zone
|
||||
test_entity = hass.states.get("scene.test_zone_dynamic_test_scene")
|
||||
assert test_entity is not None
|
||||
assert test_entity.name == "Test Zone - Dynamic Test Scene"
|
||||
assert test_entity.name == "Test Zone Dynamic Test Scene"
|
||||
assert test_entity.state == STATE_UNKNOWN
|
||||
assert test_entity.attributes["group_name"] == "Test Zone"
|
||||
assert test_entity.attributes["group_type"] == "zone"
|
||||
|
@ -33,7 +33,7 @@ async def test_scene(hass, mock_bridge_v2, v2_resources_test_data):
|
|||
# test (regular) scene for a hue room
|
||||
test_entity = hass.states.get("scene.test_room_regular_test_scene")
|
||||
assert test_entity is not None
|
||||
assert test_entity.name == "Test Room - Regular Test Scene"
|
||||
assert test_entity.name == "Test Room Regular Test Scene"
|
||||
assert test_entity.state == STATE_UNKNOWN
|
||||
assert test_entity.attributes["group_name"] == "Test Room"
|
||||
assert test_entity.attributes["group_type"] == "room"
|
||||
|
@ -42,7 +42,7 @@ async def test_scene(hass, mock_bridge_v2, v2_resources_test_data):
|
|||
assert test_entity.attributes["brightness"] == 100.0
|
||||
assert test_entity.attributes["is_dynamic"] is False
|
||||
|
||||
# scene entities should not have a device assigned
|
||||
# scene entities should have be assigned to the room/zone device/service
|
||||
ent_reg = er.async_get(hass)
|
||||
for entity_id in (
|
||||
"scene.test_zone_dynamic_test_scene",
|
||||
|
@ -50,7 +50,7 @@ async def test_scene(hass, mock_bridge_v2, v2_resources_test_data):
|
|||
):
|
||||
entity_entry = ent_reg.async_get(entity_id)
|
||||
assert entity_entry
|
||||
assert entity_entry.device_id is None
|
||||
assert entity_entry.device_id is not None
|
||||
|
||||
|
||||
async def test_scene_turn_on_service(hass, mock_bridge_v2, v2_resources_test_data):
|
||||
|
@ -144,7 +144,7 @@ async def test_scene_updates(hass, mock_bridge_v2, v2_resources_test_data):
|
|||
test_entity = hass.states.get(test_entity_id)
|
||||
assert test_entity is not None
|
||||
assert test_entity.state == STATE_UNKNOWN
|
||||
assert test_entity.name == "Test Room - Mocked Scene"
|
||||
assert test_entity.name == "Test Room Mocked Scene"
|
||||
assert test_entity.attributes["brightness"] == 65.0
|
||||
|
||||
# test update
|
||||
|
@ -156,7 +156,7 @@ async def test_scene_updates(hass, mock_bridge_v2, v2_resources_test_data):
|
|||
assert test_entity is not None
|
||||
assert test_entity.attributes["brightness"] == 35.0
|
||||
|
||||
# test entity name changes on group name change
|
||||
# # test entity name changes on group name change
|
||||
mock_bridge_v2.api.emit_event(
|
||||
"update",
|
||||
{
|
||||
|
@ -167,9 +167,9 @@ async def test_scene_updates(hass, mock_bridge_v2, v2_resources_test_data):
|
|||
)
|
||||
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.name == "Test Room 2 Mocked Scene"
|
||||
|
||||
# test delete
|
||||
# # test delete
|
||||
mock_bridge_v2.api.emit_event("delete", updated_resource)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done()
|
||||
|
|
Loading…
Add table
Reference in a new issue