core/homeassistant/components/fritzbox/light.py

186 lines
6.6 KiB
Python

"""Support for AVM FRITZ!SmartHome lightbulbs."""
from __future__ import annotations
from typing import Any, cast
from requests.exceptions import HTTPError
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP_KELVIN,
ATTR_HS_COLOR,
ColorMode,
LightEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import FritzboxDataUpdateCoordinator, FritzBoxDeviceEntity
from .common import get_coordinator
from .const import COLOR_MODE, COLOR_TEMP_MODE, LOGGER
SUPPORTED_COLOR_MODES = {ColorMode.COLOR_TEMP, ColorMode.HS}
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the FRITZ!SmartHome light from ConfigEntry."""
coordinator = get_coordinator(hass, entry.entry_id)
@callback
def _add_entities(devices: set[str] | None = None) -> None:
"""Add devices."""
if devices is None:
devices = coordinator.new_devices
if not devices:
return
async_add_entities(
FritzboxLight(coordinator, ain)
for ain in devices
if coordinator.data.devices[ain].has_lightbulb
)
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
_add_entities(set(coordinator.data.devices))
class FritzboxLight(FritzBoxDeviceEntity, LightEntity):
"""The light class for FRITZ!SmartHome lightbulbs."""
def __init__(
self,
coordinator: FritzboxDataUpdateCoordinator,
ain: str,
) -> None:
"""Initialize the FritzboxLight entity."""
super().__init__(coordinator, ain, None)
self._supported_hs: dict[int, list[int]] = {}
@property
def is_on(self) -> bool:
"""If the light is currently on or off."""
return self.data.state # type: ignore [no-any-return]
@property
def brightness(self) -> int:
"""Return the current Brightness."""
return self.data.level # type: ignore [no-any-return]
@property
def hs_color(self) -> tuple[float, float] | None:
"""Return the hs color value."""
if self.data.color_mode != COLOR_MODE:
return None
hue = self.data.hue
saturation = self.data.saturation
return (hue, float(saturation) * 100.0 / 255.0)
@property
def color_temp_kelvin(self) -> int | None:
"""Return the CT color value."""
if self.data.color_mode != COLOR_TEMP_MODE:
return None
return self.data.color_temp # type: ignore [no-any-return]
@property
def color_mode(self) -> ColorMode:
"""Return the color mode of the light."""
if self.data.has_color:
if self.data.color_mode == COLOR_MODE:
return ColorMode.HS
return ColorMode.COLOR_TEMP
if self.data.has_level:
return ColorMode.BRIGHTNESS
return ColorMode.ONOFF
@property
def supported_color_modes(self) -> set[ColorMode]:
"""Flag supported color modes."""
if self.data.has_color:
return SUPPORTED_COLOR_MODES
if self.data.has_level:
return {ColorMode.BRIGHTNESS}
return {ColorMode.ONOFF}
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the light on."""
if kwargs.get(ATTR_BRIGHTNESS) is not None:
level = kwargs[ATTR_BRIGHTNESS]
await self.hass.async_add_executor_job(self.data.set_level, level)
if kwargs.get(ATTR_HS_COLOR) is not None:
# Try setunmappedcolor first. This allows free color selection,
# but we don't know if its supported by all devices.
try:
# HA gives 0..360 for hue, fritz light only supports 0..359
unmapped_hue = int(kwargs[ATTR_HS_COLOR][0] % 360)
unmapped_saturation = round(
cast(float, kwargs[ATTR_HS_COLOR][1]) * 255.0 / 100.0
)
await self.hass.async_add_executor_job(
self.data.set_unmapped_color, (unmapped_hue, unmapped_saturation)
)
# This will raise 400 BAD REQUEST if the setunmappedcolor is not available
except HTTPError as err:
if err.response.status_code != 400:
raise
LOGGER.debug(
"fritzbox does not support method 'setunmappedcolor', fallback to"
" 'setcolor'"
)
# find supported hs values closest to what user selected
hue = min(
self._supported_hs.keys(), key=lambda x: abs(x - unmapped_hue)
)
saturation = min(
self._supported_hs[hue],
key=lambda x: abs(x - unmapped_saturation),
)
await self.hass.async_add_executor_job(
self.data.set_color, (hue, saturation)
)
if kwargs.get(ATTR_COLOR_TEMP_KELVIN) is not None:
await self.hass.async_add_executor_job(
self.data.set_color_temp, kwargs[ATTR_COLOR_TEMP_KELVIN]
)
await self.hass.async_add_executor_job(self.data.set_state_on)
await self.coordinator.async_refresh()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the light off."""
await self.hass.async_add_executor_job(self.data.set_state_off)
await self.coordinator.async_refresh()
async def async_added_to_hass(self) -> None:
"""Get light attributes from device after entity is added to hass."""
await super().async_added_to_hass()
supported_colors = await self.hass.async_add_executor_job(
self.coordinator.data.devices[self.ain].get_colors
)
supported_color_temps = await self.hass.async_add_executor_job(
self.coordinator.data.devices[self.ain].get_color_temps
)
if supported_color_temps:
# only available for color bulbs
self._attr_max_color_temp_kelvin = int(max(supported_color_temps))
self._attr_min_color_temp_kelvin = int(min(supported_color_temps))
# Fritz!DECT 500 only supports 12 values for hue, with 3 saturations each.
# Map supported colors to dict {hue: [sat1, sat2, sat3]} for easier lookup
for values in supported_colors.values():
hue = int(values[0][0])
self._supported_hs[hue] = [
int(values[0][1]),
int(values[1][1]),
int(values[2][1]),
]