core/homeassistant/components/smartthings/light.py

239 lines
8.5 KiB
Python

"""Support for lights through the SmartThings cloud API."""
from __future__ import annotations
import asyncio
from typing import Any, cast
from pysmartthings import Attribute, Capability, Command, DeviceEvent, SmartThings
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_MODE,
ATTR_COLOR_TEMP_KELVIN,
ATTR_HS_COLOR,
ATTR_TRANSITION,
ColorMode,
LightEntity,
LightEntityFeature,
brightness_supported,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
from . import FullDevice, SmartThingsConfigEntry
from .const import MAIN
from .entity import SmartThingsEntity
CAPABILITIES = (
Capability.SWITCH_LEVEL,
Capability.COLOR_CONTROL,
Capability.COLOR_TEMPERATURE,
)
async def async_setup_entry(
hass: HomeAssistant,
entry: SmartThingsConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Add lights for a config entry."""
entry_data = entry.runtime_data
async_add_entities(
SmartThingsLight(entry_data.client, device)
for device in entry_data.devices.values()
if Capability.SWITCH in device.status[MAIN]
and any(capability in device.status[MAIN] for capability in CAPABILITIES)
)
def convert_scale(
value: float, value_scale: int, target_scale: int, round_digits: int = 4
) -> float:
"""Convert a value to a different scale."""
return round(value * target_scale / value_scale, round_digits)
class SmartThingsLight(SmartThingsEntity, LightEntity, RestoreEntity):
"""Define a SmartThings Light."""
_attr_name = None
_attr_supported_color_modes: set[ColorMode]
# SmartThings does not expose this attribute, instead it's
# implemented within each device-type handler. This value is the
# lowest kelvin found supported across 20+ handlers.
_attr_min_color_temp_kelvin = 2000 # 500 mireds
# SmartThings does not expose this attribute, instead it's
# implemented within each device-type handler. This value is the
# highest kelvin found supported across 20+ handlers.
_attr_max_color_temp_kelvin = 9000 # 111 mireds
def __init__(self, client: SmartThings, device: FullDevice) -> None:
"""Initialize a SmartThingsLight."""
super().__init__(
client,
device,
{
Capability.COLOR_CONTROL,
Capability.COLOR_TEMPERATURE,
Capability.SWITCH_LEVEL,
Capability.SWITCH,
},
)
color_modes = set()
if self.supports_capability(Capability.COLOR_TEMPERATURE):
color_modes.add(ColorMode.COLOR_TEMP)
self._attr_color_mode = ColorMode.COLOR_TEMP
if self.supports_capability(Capability.COLOR_CONTROL):
color_modes.add(ColorMode.HS)
self._attr_color_mode = ColorMode.HS
if not color_modes and self.supports_capability(Capability.SWITCH_LEVEL):
color_modes.add(ColorMode.BRIGHTNESS)
if not color_modes:
color_modes.add(ColorMode.ONOFF)
if len(color_modes) == 1:
self._attr_color_mode = list(color_modes)[0]
self._attr_supported_color_modes = color_modes
features = LightEntityFeature(0)
if self.supports_capability(Capability.SWITCH_LEVEL):
features |= LightEntityFeature.TRANSITION
self._attr_supported_features = features
async def async_added_to_hass(self) -> None:
"""Run when entity about to be added to hass."""
await super().async_added_to_hass()
if (last_state := await self.async_get_last_extra_data()) is not None:
self._attr_color_mode = last_state.as_dict()[ATTR_COLOR_MODE]
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the light on."""
tasks = []
# Color temperature
if ATTR_COLOR_TEMP_KELVIN in kwargs:
tasks.append(self.async_set_color_temp(kwargs[ATTR_COLOR_TEMP_KELVIN]))
# Color
if ATTR_HS_COLOR in kwargs:
tasks.append(self.async_set_color(kwargs[ATTR_HS_COLOR]))
if tasks:
# Set temp/color first
await asyncio.gather(*tasks)
# Switch/brightness/transition
if ATTR_BRIGHTNESS in kwargs:
await self.async_set_level(
kwargs[ATTR_BRIGHTNESS], kwargs.get(ATTR_TRANSITION, 0)
)
else:
await self.execute_device_command(
Capability.SWITCH,
Command.ON,
)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the light off."""
# Switch/transition
if ATTR_TRANSITION in kwargs:
await self.async_set_level(0, int(kwargs[ATTR_TRANSITION]))
else:
await self.execute_device_command(
Capability.SWITCH,
Command.OFF,
)
def _update_attr(self) -> None:
"""Update entity attributes when the device status has changed."""
# Brightness and transition
if brightness_supported(self._attr_supported_color_modes):
if (
brightness := self.get_attribute_value(
Capability.SWITCH_LEVEL, Attribute.LEVEL
)
) is None:
self._attr_brightness = None
else:
self._attr_brightness = int(
convert_scale(
brightness,
100,
255,
0,
)
)
# Color Temperature
if ColorMode.COLOR_TEMP in self._attr_supported_color_modes:
self._attr_color_temp_kelvin = self.get_attribute_value(
Capability.COLOR_TEMPERATURE, Attribute.COLOR_TEMPERATURE
)
# Color
if ColorMode.HS in self._attr_supported_color_modes:
if (
hue := self.get_attribute_value(Capability.COLOR_CONTROL, Attribute.HUE)
) is None:
self._attr_hs_color = None
else:
self._attr_hs_color = (
convert_scale(
hue,
100,
360,
),
self.get_attribute_value(
Capability.COLOR_CONTROL, Attribute.SATURATION
),
)
async def async_set_color(self, hs_color):
"""Set the color of the device."""
hue = convert_scale(float(hs_color[0]), 360, 100)
hue = max(min(hue, 100.0), 0.0)
saturation = max(min(float(hs_color[1]), 100.0), 0.0)
await self.execute_device_command(
Capability.COLOR_CONTROL,
Command.SET_COLOR,
argument={"hue": hue, "saturation": saturation},
)
async def async_set_color_temp(self, value: int):
"""Set the color temperature of the device."""
kelvin = max(min(value, 30000), 1)
await self.execute_device_command(
Capability.COLOR_TEMPERATURE,
Command.SET_COLOR_TEMPERATURE,
argument=kelvin,
)
async def async_set_level(self, brightness: int, transition: int) -> None:
"""Set the brightness of the light over transition."""
level = int(convert_scale(brightness, 255, 100, 0))
# Due to rounding, set level to 1 (one) so we don't inadvertently
# turn off the light when a low brightness is set.
level = 1 if level == 0 and brightness > 0 else level
level = max(min(level, 100), 0)
duration = int(transition)
await self.execute_device_command(
Capability.SWITCH_LEVEL,
Command.SET_LEVEL,
argument=[level, duration],
)
def _update_handler(self, event: DeviceEvent) -> None:
"""Handle device updates."""
if event.capability in (Capability.COLOR_CONTROL, Capability.COLOR_TEMPERATURE):
self._attr_color_mode = {
Capability.COLOR_CONTROL: ColorMode.HS,
Capability.COLOR_TEMPERATURE: ColorMode.COLOR_TEMP,
}[cast(Capability, event.capability)]
super()._update_handler(event)
@property
def is_on(self) -> bool | None:
"""Return true if light is on."""
if (
state := self.get_attribute_value(Capability.SWITCH, Attribute.SWITCH)
) is None:
return None
return state == "on"