254 lines
8.7 KiB
Python
254 lines
8.7 KiB
Python
"""WiZ integration."""
|
|
from __future__ import annotations
|
|
|
|
import contextlib
|
|
import logging
|
|
from typing import Any
|
|
|
|
from pywizlight import PilotBuilder
|
|
from pywizlight.bulblibrary import BulbClass, BulbType
|
|
from pywizlight.exceptions import WizLightNotKnownBulb
|
|
from pywizlight.rgbcw import convertHSfromRGBCW
|
|
from pywizlight.scenes import get_id_from_scene_name
|
|
|
|
from homeassistant.components.light import (
|
|
ATTR_BRIGHTNESS,
|
|
ATTR_COLOR_TEMP,
|
|
ATTR_EFFECT,
|
|
ATTR_HS_COLOR,
|
|
ATTR_RGB_COLOR,
|
|
COLOR_MODE_BRIGHTNESS,
|
|
COLOR_MODE_COLOR_TEMP,
|
|
COLOR_MODE_HS,
|
|
SUPPORT_EFFECT,
|
|
LightEntity,
|
|
)
|
|
from homeassistant.config_entries import ConfigEntry
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
|
|
from homeassistant.helpers.entity import DeviceInfo
|
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
|
import homeassistant.util.color as color_utils
|
|
|
|
from .const import DOMAIN
|
|
from .models import WizData
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
DEFAULT_COLOR_MODES = {COLOR_MODE_HS, COLOR_MODE_COLOR_TEMP}
|
|
DEFAULT_MIN_MIREDS = 153
|
|
DEFAULT_MAX_MIREDS = 454
|
|
|
|
|
|
def get_supported_color_modes(bulb_type: BulbType) -> set[str]:
|
|
"""Flag supported features."""
|
|
if not bulb_type:
|
|
# fallback
|
|
return DEFAULT_COLOR_MODES
|
|
color_modes = set()
|
|
try:
|
|
features = bulb_type.features
|
|
if features.color:
|
|
color_modes.add(COLOR_MODE_HS)
|
|
if features.color_tmp:
|
|
color_modes.add(COLOR_MODE_COLOR_TEMP)
|
|
if not color_modes and features.brightness:
|
|
color_modes.add(COLOR_MODE_BRIGHTNESS)
|
|
return color_modes
|
|
except WizLightNotKnownBulb:
|
|
_LOGGER.warning("Bulb is not present in the library. Fallback to full feature")
|
|
return DEFAULT_COLOR_MODES
|
|
|
|
|
|
def supports_effects(bulb_type: BulbType) -> bool:
|
|
"""Check if a bulb supports effects."""
|
|
with contextlib.suppress(WizLightNotKnownBulb):
|
|
return bool(bulb_type.features.effect)
|
|
return True # default is true
|
|
|
|
|
|
def get_min_max_mireds(bulb_type: BulbType) -> tuple[int, int]:
|
|
"""Return the coldest and warmest color_temp that this light supports."""
|
|
if bulb_type is None:
|
|
return DEFAULT_MIN_MIREDS, DEFAULT_MAX_MIREDS
|
|
# DW bulbs have no kelvin
|
|
if bulb_type.bulb_type == BulbClass.DW:
|
|
return 0, 0
|
|
# If bulbtype is TW or RGB then return the kelvin value
|
|
try:
|
|
return color_utils.color_temperature_kelvin_to_mired(
|
|
bulb_type.kelvin_range.max
|
|
), color_utils.color_temperature_kelvin_to_mired(bulb_type.kelvin_range.min)
|
|
except WizLightNotKnownBulb:
|
|
_LOGGER.debug("Kelvin is not present in the library. Fallback to 6500")
|
|
return DEFAULT_MIN_MIREDS, DEFAULT_MAX_MIREDS
|
|
|
|
|
|
async def async_setup_entry(
|
|
hass: HomeAssistant,
|
|
entry: ConfigEntry,
|
|
async_add_entities: AddEntitiesCallback,
|
|
) -> None:
|
|
"""Set up the WiZ Platform from config_flow."""
|
|
wiz_data: WizData = hass.data[DOMAIN][entry.entry_id]
|
|
async_add_entities([WizBulbEntity(wiz_data, entry.title)])
|
|
|
|
|
|
class WizBulbEntity(CoordinatorEntity, LightEntity):
|
|
"""Representation of WiZ Light bulb."""
|
|
|
|
def __init__(self, wiz_data: WizData, name: str) -> None:
|
|
"""Initialize an WiZLight."""
|
|
super().__init__(wiz_data.coordinator)
|
|
self._light = wiz_data.bulb
|
|
bulb_type: BulbType = self._light.bulbtype
|
|
self._attr_unique_id = self._light.mac
|
|
self._attr_name = name
|
|
self._attr_effect_list = wiz_data.scenes
|
|
self._attr_min_mireds, self._attr_max_mireds = get_min_max_mireds(bulb_type)
|
|
self._attr_supported_color_modes = get_supported_color_modes(bulb_type)
|
|
if supports_effects(bulb_type):
|
|
self._attr_supported_features = SUPPORT_EFFECT
|
|
self._attr_device_info = DeviceInfo(
|
|
connections={(CONNECTION_NETWORK_MAC, self._light.mac)},
|
|
name=name,
|
|
manufacturer="WiZ",
|
|
model=bulb_type.name,
|
|
)
|
|
|
|
@property
|
|
def is_on(self) -> bool | None:
|
|
"""Return true if light is on."""
|
|
is_on: bool | None = self._light.status
|
|
return is_on
|
|
|
|
@property
|
|
def brightness(self) -> int | None:
|
|
"""Return the brightness of the light."""
|
|
if (brightness := self._light.state.get_brightness()) is None:
|
|
return None
|
|
if 0 <= int(brightness) <= 255:
|
|
return int(brightness)
|
|
_LOGGER.error("Received invalid brightness : %s. Expected: 0-255", brightness)
|
|
return None
|
|
|
|
@property
|
|
def color_mode(self) -> str:
|
|
"""Return the current color mode."""
|
|
color_modes = self.supported_color_modes
|
|
assert color_modes is not None
|
|
if (
|
|
COLOR_MODE_COLOR_TEMP in color_modes
|
|
and self._light.state.get_colortemp() is not None
|
|
):
|
|
return COLOR_MODE_COLOR_TEMP
|
|
if (
|
|
COLOR_MODE_HS in color_modes
|
|
and (rgb := self._light.state.get_rgb()) is not None
|
|
and rgb[0] is not None
|
|
):
|
|
return COLOR_MODE_HS
|
|
return COLOR_MODE_BRIGHTNESS
|
|
|
|
@property
|
|
def hs_color(self) -> tuple[float, float] | None:
|
|
"""Return the hs color value."""
|
|
colortemp = self._light.state.get_colortemp()
|
|
if colortemp is not None and colortemp != 0:
|
|
return None
|
|
if (rgb := self._light.state.get_rgb()) is None:
|
|
return None
|
|
if rgb[0] is None:
|
|
# this is the case if the temperature was changed
|
|
# do nothing until the RGB color was changed
|
|
return None
|
|
if (warmwhite := self._light.state.get_warm_white()) is None:
|
|
return None
|
|
hue_sat = convertHSfromRGBCW(rgb, warmwhite)
|
|
hue: float = hue_sat[0]
|
|
sat: float = hue_sat[1]
|
|
return hue, sat
|
|
|
|
@property
|
|
def color_temp(self) -> int | None:
|
|
"""Return the CT color value in mireds."""
|
|
colortemp = self._light.state.get_colortemp()
|
|
if colortemp is None or colortemp == 0:
|
|
return None
|
|
_LOGGER.debug(
|
|
"[wizlight %s] kelvin from the bulb: %s", self._light.ip, colortemp
|
|
)
|
|
return color_utils.color_temperature_kelvin_to_mired(colortemp)
|
|
|
|
@property
|
|
def effect(self) -> str | None:
|
|
"""Return the current effect."""
|
|
effect: str | None = self._light.state.get_scene()
|
|
return effect
|
|
|
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
|
"""Instruct the light to turn on."""
|
|
brightness = None
|
|
|
|
if ATTR_BRIGHTNESS in kwargs:
|
|
brightness = kwargs.get(ATTR_BRIGHTNESS)
|
|
|
|
if ATTR_RGB_COLOR in kwargs:
|
|
pilot = PilotBuilder(rgb=kwargs.get(ATTR_RGB_COLOR), brightness=brightness)
|
|
|
|
if ATTR_HS_COLOR in kwargs:
|
|
pilot = PilotBuilder(
|
|
hucolor=(kwargs[ATTR_HS_COLOR][0], kwargs[ATTR_HS_COLOR][1]),
|
|
brightness=brightness,
|
|
)
|
|
else:
|
|
colortemp = None
|
|
if ATTR_COLOR_TEMP in kwargs:
|
|
kelvin = color_utils.color_temperature_mired_to_kelvin(
|
|
kwargs[ATTR_COLOR_TEMP]
|
|
)
|
|
colortemp = kelvin
|
|
_LOGGER.debug(
|
|
"[wizlight %s] kelvin changed and send to bulb: %s",
|
|
self._light.ip,
|
|
colortemp,
|
|
)
|
|
|
|
sceneid = None
|
|
if ATTR_EFFECT in kwargs:
|
|
sceneid = get_id_from_scene_name(kwargs[ATTR_EFFECT])
|
|
|
|
if sceneid == 1000: # rhythm
|
|
pilot = PilotBuilder()
|
|
else:
|
|
pilot = PilotBuilder(
|
|
brightness=brightness, colortemp=colortemp, scene=sceneid
|
|
)
|
|
_LOGGER.debug(
|
|
"[wizlight %s] Pilot will be send with brightness=%s, colortemp=%s, scene=%s",
|
|
self._light.ip,
|
|
brightness,
|
|
colortemp,
|
|
sceneid,
|
|
)
|
|
|
|
sceneid = None
|
|
if ATTR_EFFECT in kwargs:
|
|
sceneid = get_id_from_scene_name(kwargs[ATTR_EFFECT])
|
|
|
|
if sceneid == 1000: # rhythm
|
|
pilot = PilotBuilder()
|
|
else:
|
|
pilot = PilotBuilder(
|
|
brightness=brightness, colortemp=colortemp, scene=sceneid
|
|
)
|
|
|
|
await self._light.turn_on(pilot)
|
|
await self.coordinator.async_request_refresh()
|
|
|
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
|
"""Instruct the light to turn off."""
|
|
await self._light.turn_off()
|
|
await self.coordinator.async_request_refresh()
|