"""Light for Shelly.""" from __future__ import annotations import asyncio import logging from typing import Any, Final, cast from aioshelly import Block import async_timeout from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_RGB_COLOR, ATTR_RGBW_COLOR, ATTR_TRANSITION, COLOR_MODE_BRIGHTNESS, COLOR_MODE_COLOR_TEMP, COLOR_MODE_ONOFF, COLOR_MODE_RGB, COLOR_MODE_RGBW, SUPPORT_EFFECT, SUPPORT_TRANSITION, LightEntity, brightness_supported, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.color import ( color_temperature_kelvin_to_mired, color_temperature_mired_to_kelvin, ) from . import ShellyDeviceWrapper from .const import ( AIOSHELLY_DEVICE_TIMEOUT_SEC, COAP, DATA_CONFIG_ENTRY, DOMAIN, FIRMWARE_PATTERN, KELVIN_MAX_VALUE, KELVIN_MIN_VALUE_COLOR, KELVIN_MIN_VALUE_WHITE, LIGHT_TRANSITION_MIN_FIRMWARE_DATE, MAX_TRANSITION_TIME, MODELS_SUPPORTING_LIGHT_TRANSITION, SHBLB_1_RGB_EFFECTS, STANDARD_RGB_EFFECTS, ) from .entity import ShellyBlockEntity from .utils import async_remove_shelly_entity _LOGGER: Final = logging.getLogger(__name__) async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up lights for device.""" wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][COAP] blocks = [] for block in wrapper.device.blocks: if block.type == "light": blocks.append(block) elif block.type == "relay": appliance_type = wrapper.device.settings["relays"][int(block.channel)].get( "appliance_type" ) if appliance_type and appliance_type.lower() == "light": blocks.append(block) unique_id = ( f'{wrapper.device.shelly["mac"]}-{block.type}_{block.channel}' ) await async_remove_shelly_entity(hass, "switch", unique_id) if not blocks: return async_add_entities(ShellyLight(wrapper, block) for block in blocks) class ShellyLight(ShellyBlockEntity, LightEntity): """Switch that controls a relay block on Shelly devices.""" def __init__(self, wrapper: ShellyDeviceWrapper, block: Block) -> None: """Initialize light.""" super().__init__(wrapper, block) self.control_result: dict[str, Any] | None = None self.mode_result: dict[str, Any] | None = None self._supported_color_modes: set[str] = set() self._supported_features: int = 0 self._min_kelvin: int = KELVIN_MIN_VALUE_WHITE self._max_kelvin: int = KELVIN_MAX_VALUE if hasattr(block, "red") and hasattr(block, "green") and hasattr(block, "blue"): self._min_kelvin = KELVIN_MIN_VALUE_COLOR if hasattr(block, "white"): self._supported_color_modes.add(COLOR_MODE_RGBW) else: self._supported_color_modes.add(COLOR_MODE_RGB) if hasattr(block, "colorTemp"): self._supported_color_modes.add(COLOR_MODE_COLOR_TEMP) if not self._supported_color_modes: if hasattr(block, "brightness") or hasattr(block, "gain"): self._supported_color_modes.add(COLOR_MODE_BRIGHTNESS) else: self._supported_color_modes.add(COLOR_MODE_ONOFF) if hasattr(block, "effect"): self._supported_features |= SUPPORT_EFFECT if wrapper.model in MODELS_SUPPORTING_LIGHT_TRANSITION: match = FIRMWARE_PATTERN.search(wrapper.device.settings.get("fw")) if ( match is not None and int(match[0]) >= LIGHT_TRANSITION_MIN_FIRMWARE_DATE ): self._supported_features |= SUPPORT_TRANSITION @property def supported_features(self) -> int: """Supported features.""" return self._supported_features @property def is_on(self) -> bool: """If light is on.""" if self.control_result: return cast(bool, self.control_result["ison"]) return bool(self.block.output) @property def mode(self) -> str: """Return the color mode of the light.""" if self.mode_result: return cast(str, self.mode_result["mode"]) if hasattr(self.block, "mode"): return cast(str, self.block.mode) if ( hasattr(self.block, "red") and hasattr(self.block, "green") and hasattr(self.block, "blue") ): return "color" return "white" @property def brightness(self) -> int: """Return the brightness of this light between 0..255.""" if self.mode == "color": if self.control_result: brightness_pct = self.control_result["gain"] else: brightness_pct = self.block.gain else: if self.control_result: brightness_pct = self.control_result["brightness"] else: brightness_pct = self.block.brightness return round(255 * brightness_pct / 100) @property def color_mode(self) -> str: """Return the color mode of the light.""" if self.mode == "color": if hasattr(self.block, "white"): return COLOR_MODE_RGBW return COLOR_MODE_RGB if hasattr(self.block, "colorTemp"): return COLOR_MODE_COLOR_TEMP if hasattr(self.block, "brightness") or hasattr(self.block, "gain"): return COLOR_MODE_BRIGHTNESS return COLOR_MODE_ONOFF @property def rgb_color(self) -> tuple[int, int, int]: """Return the rgb color value [int, int, int].""" if self.control_result: red = self.control_result["red"] green = self.control_result["green"] blue = self.control_result["blue"] else: red = self.block.red green = self.block.green blue = self.block.blue return (red, green, blue) @property def rgbw_color(self) -> tuple[int, int, int, int]: """Return the rgbw color value [int, int, int, int].""" if self.control_result: white = self.control_result["white"] else: white = self.block.white return (*self.rgb_color, white) @property def color_temp(self) -> int: """Return the CT color value in mireds.""" if self.control_result: color_temp = self.control_result["temp"] else: color_temp = self.block.colorTemp color_temp = min(self._max_kelvin, max(self._min_kelvin, color_temp)) return int(color_temperature_kelvin_to_mired(color_temp)) @property def min_mireds(self) -> int: """Return the coldest color_temp that this light supports.""" return int(color_temperature_kelvin_to_mired(self._max_kelvin)) @property def max_mireds(self) -> int: """Return the warmest color_temp that this light supports.""" return int(color_temperature_kelvin_to_mired(self._min_kelvin)) @property def supported_color_modes(self) -> set | None: """Flag supported color modes.""" return self._supported_color_modes @property def effect_list(self) -> list[str] | None: """Return the list of supported effects.""" if not self.supported_features & SUPPORT_EFFECT: return None if self.wrapper.model == "SHBLB-1": return list(SHBLB_1_RGB_EFFECTS.values()) return list(STANDARD_RGB_EFFECTS.values()) @property def effect(self) -> str | None: """Return the current effect.""" if not self.supported_features & SUPPORT_EFFECT: return None if self.control_result: effect_index = self.control_result["effect"] else: effect_index = self.block.effect if self.wrapper.model == "SHBLB-1": return SHBLB_1_RGB_EFFECTS[effect_index] return STANDARD_RGB_EFFECTS[effect_index] async def async_turn_on(self, **kwargs: Any) -> None: """Turn on light.""" if self.block.type == "relay": self.control_result = await self.set_state(turn="on") self.async_write_ha_state() return set_mode = None supported_color_modes = self._supported_color_modes params: dict[str, Any] = {"turn": "on"} if ATTR_TRANSITION in kwargs: params["transition"] = min( int(kwargs[ATTR_TRANSITION] * 1000), MAX_TRANSITION_TIME ) if ATTR_BRIGHTNESS in kwargs and brightness_supported(supported_color_modes): brightness_pct = int(100 * (kwargs[ATTR_BRIGHTNESS] + 1) / 255) if hasattr(self.block, "gain"): params["gain"] = brightness_pct if hasattr(self.block, "brightness"): params["brightness"] = brightness_pct if ATTR_COLOR_TEMP in kwargs and COLOR_MODE_COLOR_TEMP in supported_color_modes: color_temp = color_temperature_mired_to_kelvin(kwargs[ATTR_COLOR_TEMP]) color_temp = min(self._max_kelvin, max(self._min_kelvin, color_temp)) # Color temperature change - used only in white mode, switch device mode to white set_mode = "white" params["temp"] = int(color_temp) if ATTR_RGB_COLOR in kwargs and COLOR_MODE_RGB in supported_color_modes: # Color channels change - used only in color mode, switch device mode to color set_mode = "color" (params["red"], params["green"], params["blue"]) = kwargs[ATTR_RGB_COLOR] if ATTR_RGBW_COLOR in kwargs and COLOR_MODE_RGBW in supported_color_modes: # Color channels change - used only in color mode, switch device mode to color set_mode = "color" (params["red"], params["green"], params["blue"], params["white"]) = kwargs[ ATTR_RGBW_COLOR ] if ATTR_EFFECT in kwargs: # Color effect change - used only in color mode, switch device mode to color set_mode = "color" if self.wrapper.model == "SHBLB-1": effect_dict = SHBLB_1_RGB_EFFECTS else: effect_dict = STANDARD_RGB_EFFECTS if kwargs[ATTR_EFFECT] in effect_dict.values(): params["effect"] = [ k for k, v in effect_dict.items() if v == kwargs[ATTR_EFFECT] ][0] else: _LOGGER.error( "Effect '%s' not supported by device %s", kwargs[ATTR_EFFECT], self.wrapper.model, ) if await self.set_light_mode(set_mode): self.control_result = await self.set_state(**params) self.async_write_ha_state() async def async_turn_off(self, **kwargs: Any) -> None: """Turn off light.""" params: dict[str, Any] = {"turn": "off"} if ATTR_TRANSITION in kwargs: params["transition"] = min( int(kwargs[ATTR_TRANSITION] * 1000), MAX_TRANSITION_TIME ) self.control_result = await self.set_state(**params) self.async_write_ha_state() async def set_light_mode(self, set_mode: str | None) -> bool: """Change device mode color/white if mode has changed.""" if set_mode is None or self.mode == set_mode: return True _LOGGER.debug("Setting light mode for entity %s, mode: %s", self.name, set_mode) try: async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): self.mode_result = await self.wrapper.device.switch_light_mode(set_mode) except (asyncio.TimeoutError, OSError) as err: _LOGGER.error( "Setting light mode for entity %s failed, state: %s, error: %s", self.name, set_mode, repr(err), ) self.wrapper.last_update_success = False return False return True @callback def _update_callback(self) -> None: """When device updates, clear control & mode result that overrides state.""" self.control_result = None self.mode_result = None super()._update_callback()