core/homeassistant/components/hue/light.py

424 lines
13 KiB
Python
Raw Normal View History

"""Support for the Philips Hue lights."""
import asyncio
from datetime import timedelta
from functools import partial
import logging
import random
import aiohue
import async_timeout
from homeassistant.components.light import (
2019-07-31 19:25:30 +00:00
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP,
ATTR_EFFECT,
ATTR_FLASH,
ATTR_HS_COLOR,
2019-10-23 05:58:57 +00:00
ATTR_TRANSITION,
2019-07-31 19:25:30 +00:00
EFFECT_COLORLOOP,
EFFECT_RANDOM,
FLASH_LONG,
FLASH_SHORT,
SUPPORT_BRIGHTNESS,
2019-10-23 05:58:57 +00:00
SUPPORT_COLOR,
2019-07-31 19:25:30 +00:00
SUPPORT_COLOR_TEMP,
SUPPORT_EFFECT,
SUPPORT_FLASH,
SUPPORT_TRANSITION,
Light,
)
from homeassistant.core import callback
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import color
2019-10-23 05:58:57 +00:00
from .const import DOMAIN as HUE_DOMAIN, REQUEST_REFRESH_DELAY
from .helpers import remove_devices
SCAN_INTERVAL = timedelta(seconds=5)
_LOGGER = logging.getLogger(__name__)
2019-07-31 19:25:30 +00:00
SUPPORT_HUE_ON_OFF = SUPPORT_FLASH | SUPPORT_TRANSITION
SUPPORT_HUE_DIMMABLE = SUPPORT_HUE_ON_OFF | SUPPORT_BRIGHTNESS
SUPPORT_HUE_COLOR_TEMP = SUPPORT_HUE_DIMMABLE | SUPPORT_COLOR_TEMP
SUPPORT_HUE_COLOR = SUPPORT_HUE_DIMMABLE | SUPPORT_EFFECT | SUPPORT_COLOR
SUPPORT_HUE_EXTENDED = SUPPORT_HUE_COLOR_TEMP | SUPPORT_HUE_COLOR
SUPPORT_HUE = {
2019-07-31 19:25:30 +00:00
"Extended color light": SUPPORT_HUE_EXTENDED,
"Color light": SUPPORT_HUE_COLOR,
"Dimmable light": SUPPORT_HUE_DIMMABLE,
"On/Off plug-in unit": SUPPORT_HUE_ON_OFF,
"Color temperature light": SUPPORT_HUE_COLOR_TEMP,
}
2019-07-31 19:25:30 +00:00
ATTR_IS_HUE_GROUP = "is_hue_group"
GAMUT_TYPE_UNAVAILABLE = "None"
# Minimum Hue Bridge API version to support groups
# 1.4.0 introduced extended group info
# 1.12 introduced the state object for groups
# 1.13 introduced "any_on" to group state objects
GROUP_MIN_API_VERSION = (1, 13, 0)
2019-07-31 19:25:30 +00:00
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Old way of setting up Hue lights.
Can only be called when a user accidentally mentions hue platform in their
config. But even in that case it would have been ignored.
"""
pass
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]
light_coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
name="light",
update_method=partial(async_safe_fetch, bridge, bridge.api.lights.update),
update_interval=SCAN_INTERVAL,
request_refresh_debouncer=Debouncer(
bridge.hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True
),
)
# First do a refresh to see if we can reach the hub.
# Otherwise we will declare not ready.
await light_coordinator.async_refresh()
if not light_coordinator.last_update_success:
raise PlatformNotReady
update_lights = partial(
async_update_items,
bridge,
bridge.api.lights,
{},
async_add_entities,
partial(HueLight, light_coordinator, bridge, False),
)
# We add a listener after fetching the data, so manually trigger listener
light_coordinator.async_add_listener(update_lights)
update_lights()
bridge.reset_jobs.append(
lambda: light_coordinator.async_remove_listener(update_lights)
)
2019-07-31 19:25:30 +00:00
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:
2019-07-31 19:25:30 +00:00
_LOGGER.warning("Please update your Hue bridge to support groups")
allow_groups = False
if not allow_groups:
return
group_coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
name="group",
update_method=partial(async_safe_fetch, bridge, bridge.api.groups.update),
update_interval=SCAN_INTERVAL,
request_refresh_debouncer=Debouncer(
bridge.hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True
),
)
update_groups = partial(
async_update_items,
bridge,
bridge.api.groups,
{},
async_add_entities,
partial(HueLight, group_coordinator, bridge, True),
)
group_coordinator.async_add_listener(update_groups)
await group_coordinator.async_refresh()
bridge.reset_jobs.append(
lambda: group_coordinator.async_remove_listener(update_groups)
)
async def async_safe_fetch(bridge, fetch_method):
"""Safely fetch data."""
try:
with async_timeout.timeout(4):
return await bridge.async_request_call(fetch_method())
except aiohue.Unauthorized:
await bridge.handle_unauthorized_error()
raise UpdateFailed
except (asyncio.TimeoutError, aiohue.AiohueException):
raise UpdateFailed
2018-10-02 11:33:16 +00:00
@callback
def async_update_items(bridge, api, current, async_add_entities, create_item):
"""Update items."""
new_items = []
for item_id in api:
if item_id in current:
continue
current[item_id] = create_item(api[item_id])
new_items.append(current[item_id])
bridge.hass.async_create_task(remove_devices(bridge, api, current))
if new_items:
async_add_entities(new_items)
2015-06-13 23:42:09 +00:00
class HueLight(Light):
2016-03-07 21:08:21 +00:00
"""Representation of a Hue light."""
def __init__(self, coordinator, bridge, is_group, light):
2016-03-07 21:08:21 +00:00
"""Initialize the light."""
self.light = light
self.coordinator = coordinator
self.bridge = bridge
self.is_group = is_group
if is_group:
self.is_osram = False
self.is_philips = False
2020-01-07 19:56:57 +00:00
self.is_innr = False
self.gamut_typ = GAMUT_TYPE_UNAVAILABLE
self.gamut = None
else:
2019-07-31 19:25:30 +00:00
self.is_osram = light.manufacturername == "OSRAM"
self.is_philips = light.manufacturername == "Philips"
2020-01-07 19:56:57 +00:00
self.is_innr = light.manufacturername == "innr"
self.gamut_typ = self.light.colorgamuttype
self.gamut = self.light.colorgamut
_LOGGER.debug("Color gamut of %s: %s", self.name, str(self.gamut))
if self.light.swupdatestate == "readytoinstall":
err = (
"Please check for software updates of the %s "
"bulb in the Philips Hue App."
)
_LOGGER.warning(err, self.name)
if self.gamut:
if not color.check_valid_gamut(self.gamut):
err = "Color gamut of %s: %s, not valid, setting gamut to None."
_LOGGER.warning(err, self.name, str(self.gamut))
self.gamut_typ = GAMUT_TYPE_UNAVAILABLE
self.gamut = None
@property
def unique_id(self):
"""Return the unique ID of this Hue light."""
return self.light.uniqueid
@property
def should_poll(self):
"""No polling required."""
return False
@property
def device_id(self):
"""Return the ID of this Hue light."""
return self.unique_id
@property
def name(self):
"""Return the name of the Hue light."""
return self.light.name
@property
2015-06-13 23:42:09 +00:00
def brightness(self):
2016-03-07 21:08:21 +00:00
"""Return the brightness of this light between 0..255."""
if self.is_group:
2019-07-31 19:25:30 +00:00
return self.light.action.get("bri")
return self.light.state.get("bri")
@property
def _color_mode(self):
"""Return the hue color mode."""
if self.is_group:
2019-07-31 19:25:30 +00:00
return self.light.action.get("colormode")
return self.light.state.get("colormode")
2015-06-13 23:42:09 +00:00
@property
def hs_color(self):
"""Return the hs color value."""
mode = self._color_mode
source = self.light.action if self.is_group else self.light.state
2019-07-31 19:25:30 +00:00
if mode in ("xy", "hs") and "xy" in source:
return color.color_xy_to_hs(*source["xy"], self.gamut)
return None
@property
def color_temp(self):
2016-03-07 21:08:21 +00:00
"""Return the CT color value."""
# Don't return color temperature unless in color temperature mode
if self._color_mode != "ct":
return None
if self.is_group:
2019-07-31 19:25:30 +00:00
return self.light.action.get("ct")
return self.light.state.get("ct")
@property
def is_on(self):
2016-03-07 21:08:21 +00:00
"""Return true if device is on."""
if self.is_group:
2019-07-31 19:25:30 +00:00
return self.light.state["any_on"]
return self.light.state["on"]
@property
def available(self):
"""Return if light is available."""
return self.coordinator.last_update_success and (
self.is_group
or self.bridge.allow_unreachable
or self.light.state["reachable"]
2019-07-31 19:25:30 +00:00
)
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORT_HUE.get(self.light.type, SUPPORT_HUE_EXTENDED)
@property
def effect(self):
"""Return the current effect."""
2019-07-31 19:25:30 +00:00
return self.light.state.get("effect", None)
2017-02-17 18:37:45 +00:00
@property
def effect_list(self):
"""Return the list of supported effects."""
if self.is_osram:
return [EFFECT_RANDOM]
2017-02-17 18:37:45 +00:00
return [EFFECT_COLORLOOP, EFFECT_RANDOM]
@property
def device_info(self):
"""Return the device info."""
2019-07-31 19:25:30 +00:00
if self.light.type in ("LightGroup", "Room", "Luminaire", "LightSource"):
return None
return {
"identifiers": {(HUE_DOMAIN, self.device_id)},
2019-07-31 19:25:30 +00:00
"name": self.name,
"manufacturer": self.light.manufacturername,
# productname added in Hue Bridge API 1.24
# (published 03/05/2018)
2019-07-31 19:25:30 +00:00
"model": self.light.productname or self.light.modelid,
# Not yet exposed as properties in aiohue
2019-07-31 19:25:30 +00:00
"sw_version": self.light.raw["swversion"],
"via_device": (HUE_DOMAIN, self.bridge.api.config.bridgeid),
}
async def async_added_to_hass(self):
"""When entity is added to hass."""
self.coordinator.async_add_listener(self.async_write_ha_state)
async def async_will_remove_from_hass(self):
"""When entity will be removed from hass."""
self.coordinator.async_remove_listener(self.async_write_ha_state)
async def async_turn_on(self, **kwargs):
2016-03-07 21:08:21 +00:00
"""Turn the specified or all lights on."""
2019-07-31 19:25:30 +00:00
command = {"on": True}
2014-11-26 05:28:43 +00:00
if ATTR_TRANSITION in kwargs:
2019-07-31 19:25:30 +00:00
command["transitiontime"] = int(kwargs[ATTR_TRANSITION] * 10)
if ATTR_HS_COLOR in kwargs:
if self.is_osram:
2019-07-31 19:25:30 +00:00
command["hue"] = int(kwargs[ATTR_HS_COLOR][0] / 360 * 65535)
command["sat"] = int(kwargs[ATTR_HS_COLOR][1] / 100 * 255)
else:
# Philips hue bulb models respond differently to hue/sat
# requests, so we convert to XY first to ensure a consistent
# color.
2019-07-31 19:25:30 +00:00
xy_color = color.color_hs_to_xy(*kwargs[ATTR_HS_COLOR], self.gamut)
command["xy"] = xy_color
elif ATTR_COLOR_TEMP in kwargs:
temp = kwargs[ATTR_COLOR_TEMP]
2019-07-31 19:25:30 +00:00
command["ct"] = max(self.min_mireds, min(temp, self.max_mireds))
if ATTR_BRIGHTNESS in kwargs:
2019-07-31 19:25:30 +00:00
command["bri"] = kwargs[ATTR_BRIGHTNESS]
flash = kwargs.get(ATTR_FLASH)
if flash == FLASH_LONG:
2019-07-31 19:25:30 +00:00
command["alert"] = "lselect"
del command["on"]
elif flash == FLASH_SHORT:
2019-07-31 19:25:30 +00:00
command["alert"] = "select"
del command["on"]
2020-01-07 19:56:57 +00:00
elif not self.is_innr:
2019-07-31 19:25:30 +00:00
command["alert"] = "none"
if ATTR_EFFECT in kwargs:
effect = kwargs[ATTR_EFFECT]
if effect == EFFECT_COLORLOOP:
2019-07-31 19:25:30 +00:00
command["effect"] = "colorloop"
elif effect == EFFECT_RANDOM:
2019-07-31 19:25:30 +00:00
command["hue"] = random.randrange(0, 65535)
command["sat"] = random.randrange(150, 254)
else:
2019-07-31 19:25:30 +00:00
command["effect"] = "none"
if self.is_group:
await self.bridge.async_request_call(self.light.set_action(**command))
else:
await self.bridge.async_request_call(self.light.set_state(**command))
await self.coordinator.async_request_refresh()
async def async_turn_off(self, **kwargs):
2016-03-07 21:08:21 +00:00
"""Turn the specified or all lights off."""
2019-07-31 19:25:30 +00:00
command = {"on": False}
2014-11-26 05:28:43 +00:00
if ATTR_TRANSITION in kwargs:
2019-07-31 19:25:30 +00:00
command["transitiontime"] = int(kwargs[ATTR_TRANSITION] * 10)
flash = kwargs.get(ATTR_FLASH)
if flash == FLASH_LONG:
2019-07-31 19:25:30 +00:00
command["alert"] = "lselect"
del command["on"]
elif flash == FLASH_SHORT:
2019-07-31 19:25:30 +00:00
command["alert"] = "select"
del command["on"]
2020-01-07 19:56:57 +00:00
elif not self.is_innr:
2019-07-31 19:25:30 +00:00
command["alert"] = "none"
if self.is_group:
await self.bridge.async_request_call(self.light.set_action(**command))
else:
await self.bridge.async_request_call(self.light.set_state(**command))
await self.coordinator.async_request_refresh()
async def async_update(self):
"""Update the entity.
Only used by the generic entity update service.
"""
await self.coordinator.async_request_refresh()
@property
def device_state_attributes(self):
"""Return the device state attributes."""
attributes = {}
if self.is_group:
attributes[ATTR_IS_HUE_GROUP] = self.is_group
return attributes