Add suggested area to hue (#47056)

This commit is contained in:
J. Nick Koston 2021-02-26 10:35:09 -06:00 committed by GitHub
parent d8633f94f6
commit 7ab2d91bf0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 191 additions and 64 deletions

View File

@ -15,3 +15,8 @@ CONF_ALLOW_HUE_GROUPS = "allow_hue_groups"
DEFAULT_ALLOW_HUE_GROUPS = False
DEFAULT_SCENE_TRANSITION = 4
GROUP_TYPE_LIGHT_GROUP = "LightGroup"
GROUP_TYPE_ROOM = "Room"
GROUP_TYPE_LUMINAIRE = "Luminaire"
GROUP_TYPE_LIGHT_SOURCE = "LightSource"

View File

@ -36,7 +36,14 @@ from homeassistant.helpers.update_coordinator import (
)
from homeassistant.util import color
from .const import DOMAIN as HUE_DOMAIN, REQUEST_REFRESH_DELAY
from .const import (
DOMAIN as HUE_DOMAIN,
GROUP_TYPE_LIGHT_GROUP,
GROUP_TYPE_LIGHT_SOURCE,
GROUP_TYPE_LUMINAIRE,
GROUP_TYPE_ROOM,
REQUEST_REFRESH_DELAY,
)
from .helpers import remove_devices
SCAN_INTERVAL = timedelta(seconds=5)
@ -74,24 +81,35 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
"""
def create_light(item_class, coordinator, bridge, is_group, api, item_id):
def create_light(item_class, coordinator, bridge, is_group, rooms, api, item_id):
"""Create the light."""
api_item = api[item_id]
if is_group:
supported_features = 0
for light_id in api[item_id].lights:
for light_id in api_item.lights:
if light_id not in bridge.api.lights:
continue
light = bridge.api.lights[light_id]
supported_features |= SUPPORT_HUE.get(light.type, SUPPORT_HUE_EXTENDED)
supported_features = supported_features or SUPPORT_HUE_EXTENDED
else:
supported_features = SUPPORT_HUE.get(api[item_id].type, SUPPORT_HUE_EXTENDED)
return item_class(coordinator, bridge, is_group, api[item_id], supported_features)
supported_features = SUPPORT_HUE.get(api_item.type, SUPPORT_HUE_EXTENDED)
return item_class(
coordinator, bridge, is_group, api_item, supported_features, rooms
)
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Hue lights from a config entry."""
bridge = hass.data[HUE_DOMAIN][config_entry.entry_id]
api_version = tuple(int(v) for v in bridge.api.config.apiversion.split("."))
rooms = {}
allow_groups = bridge.allow_groups
supports_groups = api_version >= GROUP_MIN_API_VERSION
if allow_groups and not supports_groups:
_LOGGER.warning("Please update your Hue bridge to support groups")
light_coordinator = DataUpdateCoordinator(
hass,
@ -111,27 +129,20 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
if not light_coordinator.last_update_success:
raise PlatformNotReady
update_lights = partial(
async_update_items,
bridge,
bridge.api.lights,
{},
async_add_entities,
partial(create_light, HueLight, light_coordinator, bridge, False),
)
# We add a listener after fetching the data, so manually trigger listener
bridge.reset_jobs.append(light_coordinator.async_add_listener(update_lights))
update_lights()
api_version = tuple(int(v) for v in bridge.api.config.apiversion.split("."))
allow_groups = bridge.allow_groups
if allow_groups and api_version < GROUP_MIN_API_VERSION:
_LOGGER.warning("Please update your Hue bridge to support groups")
allow_groups = False
if not allow_groups:
if not supports_groups:
update_lights_without_group_support = partial(
async_update_items,
bridge,
bridge.api.lights,
{},
async_add_entities,
partial(create_light, HueLight, light_coordinator, bridge, False, rooms),
None,
)
# We add a listener after fetching the data, so manually trigger listener
bridge.reset_jobs.append(
light_coordinator.async_add_listener(update_lights_without_group_support)
)
return
group_coordinator = DataUpdateCoordinator(
@ -145,17 +156,69 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
),
)
update_groups = partial(
if allow_groups:
update_groups = partial(
async_update_items,
bridge,
bridge.api.groups,
{},
async_add_entities,
partial(create_light, HueLight, group_coordinator, bridge, True, None),
None,
)
bridge.reset_jobs.append(group_coordinator.async_add_listener(update_groups))
cancel_update_rooms_listener = None
@callback
def _async_update_rooms():
"""Update rooms."""
nonlocal cancel_update_rooms_listener
rooms.clear()
for item_id in bridge.api.groups:
group = bridge.api.groups[item_id]
if group.type != GROUP_TYPE_ROOM:
continue
for light_id in group.lights:
rooms[light_id] = group.name
# Once we do a rooms update, we cancel the listener
# until the next time lights are added
bridge.reset_jobs.remove(cancel_update_rooms_listener)
cancel_update_rooms_listener() # pylint: disable=not-callable
cancel_update_rooms_listener = None
@callback
def _setup_rooms_listener():
nonlocal cancel_update_rooms_listener
if cancel_update_rooms_listener is not None:
# If there are new lights added before _async_update_rooms
# is called we should not add another listener
return
cancel_update_rooms_listener = group_coordinator.async_add_listener(
_async_update_rooms
)
bridge.reset_jobs.append(cancel_update_rooms_listener)
_setup_rooms_listener()
await group_coordinator.async_refresh()
update_lights_with_group_support = partial(
async_update_items,
bridge,
bridge.api.groups,
bridge.api.lights,
{},
async_add_entities,
partial(create_light, HueLight, group_coordinator, bridge, True),
partial(create_light, HueLight, light_coordinator, bridge, False, rooms),
_setup_rooms_listener,
)
bridge.reset_jobs.append(group_coordinator.async_add_listener(update_groups))
await group_coordinator.async_refresh()
# We add a listener after fetching the data, so manually trigger listener
bridge.reset_jobs.append(
light_coordinator.async_add_listener(update_lights_with_group_support)
)
update_lights_with_group_support()
async def async_safe_fetch(bridge, fetch_method):
@ -171,7 +234,9 @@ async def async_safe_fetch(bridge, fetch_method):
@callback
def async_update_items(bridge, api, current, async_add_entities, create_item):
def async_update_items(
bridge, api, current, async_add_entities, create_item, new_items_callback
):
"""Update items."""
new_items = []
@ -185,6 +250,9 @@ def async_update_items(bridge, api, current, async_add_entities, create_item):
bridge.hass.async_create_task(remove_devices(bridge, api, current))
if new_items:
# This is currently used to setup the listener to update rooms
if new_items_callback:
new_items_callback()
async_add_entities(new_items)
@ -201,13 +269,14 @@ def hass_to_hue_brightness(value):
class HueLight(CoordinatorEntity, LightEntity):
"""Representation of a Hue light."""
def __init__(self, coordinator, bridge, is_group, light, supported_features):
def __init__(self, coordinator, bridge, is_group, light, supported_features, rooms):
"""Initialize the light."""
super().__init__(coordinator)
self.light = light
self.bridge = bridge
self.is_group = is_group
self._supported_features = supported_features
self._rooms = rooms
if is_group:
self.is_osram = False
@ -355,10 +424,15 @@ class HueLight(CoordinatorEntity, LightEntity):
@property
def device_info(self):
"""Return the device info."""
if self.light.type in ("LightGroup", "Room", "Luminaire", "LightSource"):
if self.light.type in (
GROUP_TYPE_LIGHT_GROUP,
GROUP_TYPE_ROOM,
GROUP_TYPE_LUMINAIRE,
GROUP_TYPE_LIGHT_SOURCE,
):
return None
return {
info = {
"identifiers": {(HUE_DOMAIN, self.device_id)},
"name": self.name,
"manufacturer": self.light.manufacturername,
@ -370,6 +444,11 @@ class HueLight(CoordinatorEntity, LightEntity):
"via_device": (HUE_DOMAIN, self.bridge.api.config.bridgeid),
}
if self.light.id in self._rooms:
info["suggested_area"] = self._rooms[self.light.id]
return info
async def async_turn_on(self, **kwargs):
"""Turn the specified or all lights on."""
command = {"on": True}

View File

@ -7,6 +7,12 @@ import aiohue
from homeassistant import config_entries
from homeassistant.components import hue
from homeassistant.components.hue import light as hue_light
from homeassistant.helpers.device_registry import (
async_get_registry as async_get_device_registry,
)
from homeassistant.helpers.entity_registry import (
async_get_registry as async_get_entity_registry,
)
from homeassistant.util import color
HUE_LIGHT_NS = "homeassistant.components.light.hue."
@ -211,8 +217,10 @@ async def test_no_lights_or_groups(hass, mock_bridge):
async def test_lights(hass, mock_bridge):
"""Test the update_lights function with some lights."""
mock_bridge.mock_light_responses.append(LIGHT_RESPONSE)
mock_bridge.mock_group_responses.append(GROUP_RESPONSE)
await setup_bridge(hass, mock_bridge)
assert len(mock_bridge.mock_requests) == 1
assert len(mock_bridge.mock_requests) == 2
# 2 lights
assert len(hass.states.async_all()) == 2
@ -230,6 +238,8 @@ async def test_lights(hass, mock_bridge):
async def test_lights_color_mode(hass, mock_bridge):
"""Test that lights only report appropriate color mode."""
mock_bridge.mock_light_responses.append(LIGHT_RESPONSE)
mock_bridge.mock_group_responses.append(GROUP_RESPONSE)
await setup_bridge(hass, mock_bridge)
lamp_1 = hass.states.get("light.hue_lamp_1")
@ -249,8 +259,8 @@ async def test_lights_color_mode(hass, mock_bridge):
await hass.services.async_call(
"light", "turn_on", {"entity_id": "light.hue_lamp_2"}, blocking=True
)
# 2x light update, 1 turn on request
assert len(mock_bridge.mock_requests) == 3
# 2x light update, 1 group update, 1 turn on request
assert len(mock_bridge.mock_requests) == 4
lamp_1 = hass.states.get("light.hue_lamp_1")
assert lamp_1 is not None
@ -332,9 +342,10 @@ async def test_new_group_discovered(hass, mock_bridge):
async def test_new_light_discovered(hass, mock_bridge):
"""Test if 2nd update has a new light."""
mock_bridge.mock_light_responses.append(LIGHT_RESPONSE)
mock_bridge.mock_group_responses.append(GROUP_RESPONSE)
await setup_bridge(hass, mock_bridge)
assert len(mock_bridge.mock_requests) == 1
assert len(mock_bridge.mock_requests) == 2
assert len(hass.states.async_all()) == 2
new_light_response = dict(LIGHT_RESPONSE)
@ -366,8 +377,8 @@ async def test_new_light_discovered(hass, mock_bridge):
await hass.services.async_call(
"light", "turn_on", {"entity_id": "light.hue_lamp_1"}, blocking=True
)
# 2x light update, 1 turn on request
assert len(mock_bridge.mock_requests) == 3
# 2x light update, 1 group update, 1 turn on request
assert len(mock_bridge.mock_requests) == 4
assert len(hass.states.async_all()) == 3
light = hass.states.get("light.hue_lamp_3")
@ -407,9 +418,10 @@ async def test_group_removed(hass, mock_bridge):
async def test_light_removed(hass, mock_bridge):
"""Test if 2nd update has removed light."""
mock_bridge.mock_light_responses.append(LIGHT_RESPONSE)
mock_bridge.mock_group_responses.append(GROUP_RESPONSE)
await setup_bridge(hass, mock_bridge)
assert len(mock_bridge.mock_requests) == 1
assert len(mock_bridge.mock_requests) == 2
assert len(hass.states.async_all()) == 2
mock_bridge.mock_light_responses.clear()
@ -420,8 +432,8 @@ async def test_light_removed(hass, mock_bridge):
"light", "turn_on", {"entity_id": "light.hue_lamp_1"}, blocking=True
)
# 2x light update, 1 turn on request
assert len(mock_bridge.mock_requests) == 3
# 2x light update, 1 group update, 1 turn on request
assert len(mock_bridge.mock_requests) == 4
assert len(hass.states.async_all()) == 1
light = hass.states.get("light.hue_lamp_1")
@ -487,9 +499,10 @@ async def test_other_group_update(hass, mock_bridge):
async def test_other_light_update(hass, mock_bridge):
"""Test changing one light that will impact state of other light."""
mock_bridge.mock_light_responses.append(LIGHT_RESPONSE)
mock_bridge.mock_group_responses.append(GROUP_RESPONSE)
await setup_bridge(hass, mock_bridge)
assert len(mock_bridge.mock_requests) == 1
assert len(mock_bridge.mock_requests) == 2
assert len(hass.states.async_all()) == 2
lamp_2 = hass.states.get("light.hue_lamp_2")
@ -526,8 +539,8 @@ async def test_other_light_update(hass, mock_bridge):
await hass.services.async_call(
"light", "turn_on", {"entity_id": "light.hue_lamp_1"}, blocking=True
)
# 2x light update, 1 turn on request
assert len(mock_bridge.mock_requests) == 3
# 2x light update, 1 group update, 1 turn on request
assert len(mock_bridge.mock_requests) == 4
assert len(hass.states.async_all()) == 2
lamp_2 = hass.states.get("light.hue_lamp_2")
@ -549,7 +562,6 @@ async def test_update_timeout(hass, mock_bridge):
async def test_update_unauthorized(hass, mock_bridge):
"""Test bridge marked as not authorized if unauthorized during update."""
mock_bridge.api.lights.update = Mock(side_effect=aiohue.Unauthorized)
mock_bridge.api.groups.update = Mock(side_effect=aiohue.Unauthorized)
await setup_bridge(hass, mock_bridge)
assert len(mock_bridge.mock_requests) == 0
assert len(hass.states.async_all()) == 0
@ -559,6 +571,8 @@ async def test_update_unauthorized(hass, mock_bridge):
async def test_light_turn_on_service(hass, mock_bridge):
"""Test calling the turn on service on a light."""
mock_bridge.mock_light_responses.append(LIGHT_RESPONSE)
mock_bridge.mock_group_responses.append(GROUP_RESPONSE)
await setup_bridge(hass, mock_bridge)
light = hass.states.get("light.hue_lamp_2")
assert light is not None
@ -575,10 +589,10 @@ async def test_light_turn_on_service(hass, mock_bridge):
{"entity_id": "light.hue_lamp_2", "brightness": 100, "color_temp": 300},
blocking=True,
)
# 2x light update, 1 turn on request
assert len(mock_bridge.mock_requests) == 3
# 2x light update, 1 group update, 1 turn on request
assert len(mock_bridge.mock_requests) == 4
assert mock_bridge.mock_requests[1]["json"] == {
assert mock_bridge.mock_requests[2]["json"] == {
"bri": 100,
"on": True,
"ct": 300,
@ -599,9 +613,9 @@ async def test_light_turn_on_service(hass, mock_bridge):
blocking=True,
)
assert len(mock_bridge.mock_requests) == 5
assert len(mock_bridge.mock_requests) == 6
assert mock_bridge.mock_requests[3]["json"] == {
assert mock_bridge.mock_requests[4]["json"] == {
"on": True,
"xy": (0.138, 0.08),
"alert": "none",
@ -611,6 +625,8 @@ async def test_light_turn_on_service(hass, mock_bridge):
async def test_light_turn_off_service(hass, mock_bridge):
"""Test calling the turn on service on a light."""
mock_bridge.mock_light_responses.append(LIGHT_RESPONSE)
mock_bridge.mock_group_responses.append(GROUP_RESPONSE)
await setup_bridge(hass, mock_bridge)
light = hass.states.get("light.hue_lamp_1")
assert light is not None
@ -624,10 +640,11 @@ async def test_light_turn_off_service(hass, mock_bridge):
await hass.services.async_call(
"light", "turn_off", {"entity_id": "light.hue_lamp_1"}, blocking=True
)
# 2x light update, 1 turn on request
assert len(mock_bridge.mock_requests) == 3
assert mock_bridge.mock_requests[1]["json"] == {"on": False, "alert": "none"}
# 2x light update, 1 for group update, 1 turn on request
assert len(mock_bridge.mock_requests) == 4
assert mock_bridge.mock_requests[2]["json"] == {"on": False, "alert": "none"}
assert len(hass.states.async_all()) == 2
@ -649,6 +666,7 @@ def test_available():
bridge=Mock(allow_unreachable=False),
is_group=False,
supported_features=hue_light.SUPPORT_HUE_EXTENDED,
rooms={},
)
assert light.available is False
@ -664,6 +682,7 @@ def test_available():
bridge=Mock(allow_unreachable=True),
is_group=False,
supported_features=hue_light.SUPPORT_HUE_EXTENDED,
rooms={},
)
assert light.available is True
@ -679,6 +698,7 @@ def test_available():
bridge=Mock(allow_unreachable=False),
is_group=True,
supported_features=hue_light.SUPPORT_HUE_EXTENDED,
rooms={},
)
assert light.available is True
@ -697,6 +717,7 @@ def test_hs_color():
bridge=Mock(),
is_group=False,
supported_features=hue_light.SUPPORT_HUE_EXTENDED,
rooms={},
)
assert light.hs_color is None
@ -712,6 +733,7 @@ def test_hs_color():
bridge=Mock(),
is_group=False,
supported_features=hue_light.SUPPORT_HUE_EXTENDED,
rooms={},
)
assert light.hs_color is None
@ -727,6 +749,7 @@ def test_hs_color():
bridge=Mock(),
is_group=False,
supported_features=hue_light.SUPPORT_HUE_EXTENDED,
rooms={},
)
assert light.hs_color == color.color_xy_to_hs(0.4, 0.5, LIGHT_GAMUT)
@ -742,7 +765,7 @@ async def test_group_features(hass, mock_bridge):
"1": {
"name": "Group 1",
"lights": ["1", "2"],
"type": "Room",
"type": "LightGroup",
"action": {
"on": True,
"bri": 254,
@ -757,8 +780,8 @@ async def test_group_features(hass, mock_bridge):
"state": {"any_on": True, "all_on": False},
},
"2": {
"name": "Group 2",
"lights": ["3", "4"],
"name": "Living Room",
"lights": ["2", "3"],
"type": "Room",
"action": {
"on": True,
@ -774,8 +797,8 @@ async def test_group_features(hass, mock_bridge):
"state": {"any_on": True, "all_on": False},
},
"3": {
"name": "Group 3",
"lights": ["1", "3"],
"name": "Dining Room",
"lights": ["4"],
"type": "Room",
"action": {
"on": True,
@ -900,6 +923,7 @@ async def test_group_features(hass, mock_bridge):
mock_bridge.mock_light_responses.append(light_response)
mock_bridge.mock_group_responses.append(group_response)
await setup_bridge(hass, mock_bridge)
assert len(mock_bridge.mock_requests) == 2
color_temp_feature = hue_light.SUPPORT_HUE["Color temperature light"]
extended_color_feature = hue_light.SUPPORT_HUE["Extended color light"]
@ -907,8 +931,27 @@ async def test_group_features(hass, mock_bridge):
group_1 = hass.states.get("light.group_1")
assert group_1.attributes["supported_features"] == color_temp_feature
group_2 = hass.states.get("light.group_2")
group_2 = hass.states.get("light.living_room")
assert group_2.attributes["supported_features"] == extended_color_feature
group_3 = hass.states.get("light.group_3")
group_3 = hass.states.get("light.dining_room")
assert group_3.attributes["supported_features"] == extended_color_feature
entity_registry = await async_get_entity_registry(hass)
device_registry = await async_get_device_registry(hass)
entry = entity_registry.async_get("light.hue_lamp_1")
device_entry = device_registry.async_get(entry.device_id)
assert device_entry.suggested_area is None
entry = entity_registry.async_get("light.hue_lamp_2")
device_entry = device_registry.async_get(entry.device_id)
assert device_entry.suggested_area == "Living Room"
entry = entity_registry.async_get("light.hue_lamp_3")
device_entry = device_registry.async_get(entry.device_id)
assert device_entry.suggested_area == "Living Room"
entry = entity_registry.async_get("light.hue_lamp_4")
device_entry = device_registry.async_get(entry.device_id)
assert device_entry.suggested_area == "Dining Room"