Files
core/homeassistant/components/hue/v2/light.py
2022-03-21 20:42:07 -07:00

255 lines
9.2 KiB
Python

"""Support for Hue lights."""
from __future__ import annotations
from typing import Any
from aiohue import HueBridgeV2
from aiohue.v2.controllers.events import EventType
from aiohue.v2.controllers.lights import LightsController
from aiohue.v2.models.light import Light
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP,
ATTR_FLASH,
ATTR_TRANSITION,
ATTR_XY_COLOR,
COLOR_MODE_BRIGHTNESS,
COLOR_MODE_COLOR_TEMP,
COLOR_MODE_ONOFF,
COLOR_MODE_XY,
FLASH_SHORT,
SUPPORT_FLASH,
SUPPORT_TRANSITION,
LightEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from ..bridge import HueBridge
from ..const import DOMAIN
from .entity import HueBaseEntity
from .helpers import (
normalize_hue_brightness,
normalize_hue_colortemp,
normalize_hue_transition,
)
ALLOWED_ERRORS = [
"device (light) has communication issues, command (on) may not have effect",
'device (light) is "soft off", command (on) may not have effect',
"device (light) has communication issues, command (.on) may not have effect",
'device (light) is "soft off", command (.on) may not have effect',
"device (light) has communication issues, command (.on.on) may not have effect",
'device (light) is "soft off", command (.on.on) may not have effect',
]
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Hue Light from Config Entry."""
bridge: HueBridge = hass.data[DOMAIN][config_entry.entry_id]
api: HueBridgeV2 = bridge.api
controller: LightsController = api.lights
@callback
def async_add_light(event_type: EventType, resource: Light) -> None:
"""Add Hue Light."""
light = HueLight(bridge, controller, resource)
async_add_entities([light])
# add all current items in controller
for light in controller:
async_add_light(EventType.RESOURCE_ADDED, resource=light)
# register listener for new lights
config_entry.async_on_unload(
controller.subscribe(async_add_light, event_filter=EventType.RESOURCE_ADDED)
)
class HueLight(HueBaseEntity, LightEntity):
"""Representation of a Hue light."""
def __init__(
self, bridge: HueBridge, controller: LightsController, resource: Light
) -> None:
"""Initialize the light."""
super().__init__(bridge, controller, resource)
if self.resource.alert and self.resource.alert.action_values:
self._attr_supported_features |= SUPPORT_FLASH
self.resource = resource
self.controller = controller
self._supported_color_modes = set()
if self.resource.supports_color:
self._supported_color_modes.add(COLOR_MODE_XY)
if self.resource.supports_color_temperature:
self._supported_color_modes.add(COLOR_MODE_COLOR_TEMP)
if self.resource.supports_dimming:
if len(self._supported_color_modes) == 0:
# only add color mode brightness if no color variants
self._supported_color_modes.add(COLOR_MODE_BRIGHTNESS)
# support transition if brightness control
self._attr_supported_features |= SUPPORT_TRANSITION
self._last_xy: tuple[float, float] | None = self.xy_color
self._last_color_temp: int = self.color_temp
self._set_color_mode()
@property
def brightness(self) -> int | None:
"""Return the brightness of this light between 0..255."""
if dimming := self.resource.dimming:
# Hue uses a range of [0, 100] to control brightness.
return round((dimming.brightness / 100) * 255)
return None
@property
def is_on(self) -> bool:
"""Return true if device is on (brightness above 0)."""
return self.resource.on.on
@property
def xy_color(self) -> tuple[float, float] | None:
"""Return the xy color."""
if color := self.resource.color:
return (color.xy.x, color.xy.y)
return None
@property
def color_temp(self) -> int:
"""Return the color temperature."""
if color_temp := self.resource.color_temperature:
return color_temp.mirek
return 0
@property
def min_mireds(self) -> int:
"""Return the coldest color_temp that this light supports."""
if color_temp := self.resource.color_temperature:
return color_temp.mirek_schema.mirek_minimum
return 0
@property
def max_mireds(self) -> int:
"""Return the warmest color_temp that this light supports."""
if color_temp := self.resource.color_temperature:
return color_temp.mirek_schema.mirek_maximum
return 0
@property
def supported_color_modes(self) -> set | None:
"""Flag supported features."""
return self._supported_color_modes
@property
def extra_state_attributes(self) -> dict[str, str] | None:
"""Return the optional state attributes."""
return {
"mode": self.resource.mode.value,
"dynamics": self.resource.dynamics.status.value,
}
@callback
def on_update(self) -> None:
"""Call on update event."""
self._set_color_mode()
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the device on."""
transition = normalize_hue_transition(kwargs.get(ATTR_TRANSITION))
xy_color = kwargs.get(ATTR_XY_COLOR)
color_temp = normalize_hue_colortemp(kwargs.get(ATTR_COLOR_TEMP))
brightness = normalize_hue_brightness(kwargs.get(ATTR_BRIGHTNESS))
flash = kwargs.get(ATTR_FLASH)
if flash is not None:
await self.async_set_flash(flash)
# flash can not be sent with other commands at the same time or result will be flaky
# Hue's default behavior is that a light returns to its previous state for short
# flash (identify) and the light is kept turned on for long flash (breathe effect)
# Why is this flash alert/effect hidden in the turn_on/off commands ?
return
await self.bridge.async_request_call(
self.controller.set_state,
id=self.resource.id,
on=True,
brightness=brightness,
color_xy=xy_color,
color_temp=color_temp,
transition_time=transition,
allowed_errors=ALLOWED_ERRORS,
)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the light off."""
transition = normalize_hue_transition(kwargs.get(ATTR_TRANSITION))
flash = kwargs.get(ATTR_FLASH)
if flash is not None:
await self.async_set_flash(flash)
# flash can not be sent with other commands at the same time or result will be flaky
# Hue's default behavior is that a light returns to its previous state for short
# flash (identify) and the light is kept turned on for long flash (breathe effect)
return
await self.bridge.async_request_call(
self.controller.set_state,
id=self.resource.id,
on=False,
transition_time=transition,
allowed_errors=ALLOWED_ERRORS,
)
async def async_set_flash(self, flash: str) -> None:
"""Send flash command to light."""
await self.bridge.async_request_call(
self.controller.set_flash,
id=self.resource.id,
short=flash == FLASH_SHORT,
)
@callback
def _set_color_mode(self) -> None:
"""Set current colormode of light."""
last_xy = self._last_xy
last_color_temp = self._last_color_temp
self._last_xy = self.xy_color
self._last_color_temp = self.color_temp
# Certified Hue lights return `mired_valid` to indicate CT is active
if color_temp := self.resource.color_temperature:
if color_temp.mirek_valid and color_temp.mirek is not None:
self._attr_color_mode = COLOR_MODE_COLOR_TEMP
return
# Non-certified lights do not report their current color mode correctly
# so we keep track of the color values to determine which is active
if last_color_temp != self.color_temp:
self._attr_color_mode = COLOR_MODE_COLOR_TEMP
return
if last_xy != self.xy_color:
self._attr_color_mode = COLOR_MODE_XY
return
# if we didn't detect any changes, abort and use previous values
if self._attr_color_mode is not None:
return
# color mode not yet determined, work it out here
# Note that for lights that do not correctly report `mirek_valid`
# we might have an invalid startup state which will be auto corrected
if self.resource.supports_color:
self._attr_color_mode = COLOR_MODE_XY
elif self.resource.supports_color_temperature:
self._attr_color_mode = COLOR_MODE_COLOR_TEMP
elif self.resource.supports_dimming:
self._attr_color_mode = COLOR_MODE_BRIGHTNESS
else:
# fallback to on_off
self._attr_color_mode = COLOR_MODE_ONOFF