1
mirror of https://github.com/home-assistant/core synced 2024-08-02 23:40:32 +02:00
ha-core/homeassistant/components/xiaomi_miio/light.py
epenet cb135bc889
Add basic type hints to xiaomi_miio (#62889)
Co-authored-by: epenet <epenet@users.noreply.github.com>
2021-12-28 00:15:40 +01:00

1087 lines
36 KiB
Python

"""Support for Xiaomi Philips Lights."""
import asyncio
import datetime
from datetime import timedelta
from functools import partial
import logging
from math import ceil
from miio import Ceil, DeviceException, PhilipsBulb, PhilipsEyecare, PhilipsMoonlight
from miio.gateway.gateway import (
GATEWAY_MODEL_AC_V1,
GATEWAY_MODEL_AC_V2,
GATEWAY_MODEL_AC_V3,
GatewayException,
)
import voluptuous as vol
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP,
ATTR_HS_COLOR,
SUPPORT_BRIGHTNESS,
SUPPORT_COLOR,
SUPPORT_COLOR_TEMP,
LightEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_TOKEN
from homeassistant.core import HomeAssistant, ServiceCall
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import color, dt
from .const import (
CONF_DEVICE,
CONF_FLOW_TYPE,
CONF_GATEWAY,
CONF_MODEL,
DOMAIN,
KEY_COORDINATOR,
MODELS_LIGHT_BULB,
MODELS_LIGHT_CEILING,
MODELS_LIGHT_EYECARE,
MODELS_LIGHT_MONO,
MODELS_LIGHT_MOON,
SERVICE_EYECARE_MODE_OFF,
SERVICE_EYECARE_MODE_ON,
SERVICE_NIGHT_LIGHT_MODE_OFF,
SERVICE_NIGHT_LIGHT_MODE_ON,
SERVICE_REMINDER_OFF,
SERVICE_REMINDER_ON,
SERVICE_SET_DELAYED_TURN_OFF,
SERVICE_SET_SCENE,
)
from .device import XiaomiMiioEntity
from .gateway import XiaomiGatewayDevice
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = "Xiaomi Philips Light"
DATA_KEY = "light.xiaomi_miio"
# The light does not accept cct values < 1
CCT_MIN = 1
CCT_MAX = 100
DELAYED_TURN_OFF_MAX_DEVIATION_SECONDS = 4
DELAYED_TURN_OFF_MAX_DEVIATION_MINUTES = 1
SUCCESS = ["ok"]
ATTR_SCENE = "scene"
ATTR_DELAYED_TURN_OFF = "delayed_turn_off"
ATTR_TIME_PERIOD = "time_period"
ATTR_NIGHT_LIGHT_MODE = "night_light_mode"
ATTR_AUTOMATIC_COLOR_TEMPERATURE = "automatic_color_temperature"
ATTR_REMINDER = "reminder"
ATTR_EYECARE_MODE = "eyecare_mode"
# Moonlight
ATTR_SLEEP_ASSISTANT = "sleep_assistant"
ATTR_SLEEP_OFF_TIME = "sleep_off_time"
ATTR_TOTAL_ASSISTANT_SLEEP_TIME = "total_assistant_sleep_time"
ATTR_BAND_SLEEP = "band_sleep"
ATTR_BAND = "band"
XIAOMI_MIIO_SERVICE_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids})
SERVICE_SCHEMA_SET_SCENE = XIAOMI_MIIO_SERVICE_SCHEMA.extend(
{vol.Required(ATTR_SCENE): vol.All(vol.Coerce(int), vol.Clamp(min=1, max=6))}
)
SERVICE_SCHEMA_SET_DELAYED_TURN_OFF = XIAOMI_MIIO_SERVICE_SCHEMA.extend(
{vol.Required(ATTR_TIME_PERIOD): cv.positive_time_period}
)
SERVICE_TO_METHOD = {
SERVICE_SET_DELAYED_TURN_OFF: {
"method": "async_set_delayed_turn_off",
"schema": SERVICE_SCHEMA_SET_DELAYED_TURN_OFF,
},
SERVICE_SET_SCENE: {
"method": "async_set_scene",
"schema": SERVICE_SCHEMA_SET_SCENE,
},
SERVICE_REMINDER_ON: {"method": "async_reminder_on"},
SERVICE_REMINDER_OFF: {"method": "async_reminder_off"},
SERVICE_NIGHT_LIGHT_MODE_ON: {"method": "async_night_light_mode_on"},
SERVICE_NIGHT_LIGHT_MODE_OFF: {"method": "async_night_light_mode_off"},
SERVICE_EYECARE_MODE_ON: {"method": "async_eyecare_mode_on"},
SERVICE_EYECARE_MODE_OFF: {"method": "async_eyecare_mode_off"},
}
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Xiaomi light from a config entry."""
entities = []
if config_entry.data[CONF_FLOW_TYPE] == CONF_GATEWAY:
gateway = hass.data[DOMAIN][config_entry.entry_id][CONF_GATEWAY]
# Gateway light
if gateway.model not in [
GATEWAY_MODEL_AC_V1,
GATEWAY_MODEL_AC_V2,
GATEWAY_MODEL_AC_V3,
]:
entities.append(
XiaomiGatewayLight(gateway, config_entry.title, config_entry.unique_id)
)
# Gateway sub devices
sub_devices = gateway.devices
coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR]
for sub_device in sub_devices.values():
if sub_device.device_type == "LightBulb":
entities.append(
XiaomiGatewayBulb(coordinator, sub_device, config_entry)
)
if config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE:
if DATA_KEY not in hass.data:
hass.data[DATA_KEY] = {}
host = config_entry.data[CONF_HOST]
token = config_entry.data[CONF_TOKEN]
name = config_entry.title
model = config_entry.data[CONF_MODEL]
unique_id = config_entry.unique_id
_LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5])
if model in MODELS_LIGHT_EYECARE:
light = PhilipsEyecare(host, token)
entity = XiaomiPhilipsEyecareLamp(name, light, config_entry, unique_id)
entities.append(entity)
hass.data[DATA_KEY][host] = entity
entities.append(
XiaomiPhilipsEyecareLampAmbientLight(
name, light, config_entry, unique_id
)
)
# The ambient light doesn't expose additional services.
# A hass.data[DATA_KEY] entry isn't needed.
elif model in MODELS_LIGHT_CEILING:
light = Ceil(host, token)
entity = XiaomiPhilipsCeilingLamp(name, light, config_entry, unique_id)
entities.append(entity)
hass.data[DATA_KEY][host] = entity
elif model in MODELS_LIGHT_MOON:
light = PhilipsMoonlight(host, token)
entity = XiaomiPhilipsMoonlightLamp(name, light, config_entry, unique_id)
entities.append(entity)
hass.data[DATA_KEY][host] = entity
elif model in MODELS_LIGHT_BULB:
light = PhilipsBulb(host, token)
entity = XiaomiPhilipsBulb(name, light, config_entry, unique_id)
entities.append(entity)
hass.data[DATA_KEY][host] = entity
elif model in MODELS_LIGHT_MONO:
light = PhilipsBulb(host, token)
entity = XiaomiPhilipsGenericLight(name, light, config_entry, unique_id)
entities.append(entity)
hass.data[DATA_KEY][host] = entity
else:
_LOGGER.error(
"Unsupported device found! Please create an issue at "
"https://github.com/syssi/philipslight/issues "
"and provide the following data: %s",
model,
)
return
async def async_service_handler(service: ServiceCall) -> None:
"""Map services to methods on Xiaomi Philips Lights."""
method = SERVICE_TO_METHOD.get(service.service)
params = {
key: value
for key, value in service.data.items()
if key != ATTR_ENTITY_ID
}
if entity_ids := service.data.get(ATTR_ENTITY_ID):
target_devices = [
dev
for dev in hass.data[DATA_KEY].values()
if dev.entity_id in entity_ids
]
else:
target_devices = hass.data[DATA_KEY].values()
update_tasks = []
for target_device in target_devices:
if not hasattr(target_device, method["method"]):
continue
await getattr(target_device, method["method"])(**params)
update_tasks.append(target_device.async_update_ha_state(True))
if update_tasks:
await asyncio.wait(update_tasks)
for xiaomi_miio_service, method in SERVICE_TO_METHOD.items():
schema = method.get("schema", XIAOMI_MIIO_SERVICE_SCHEMA)
hass.services.async_register(
DOMAIN, xiaomi_miio_service, async_service_handler, schema=schema
)
async_add_entities(entities, update_before_add=True)
class XiaomiPhilipsAbstractLight(XiaomiMiioEntity, LightEntity):
"""Representation of a Abstract Xiaomi Philips Light."""
def __init__(self, name, device, entry, unique_id):
"""Initialize the light device."""
super().__init__(name, device, entry, unique_id)
self._brightness = None
self._available = False
self._state = None
self._state_attrs = {}
@property
def available(self):
"""Return true when state is known."""
return self._available
@property
def extra_state_attributes(self):
"""Return the state attributes of the device."""
return self._state_attrs
@property
def is_on(self):
"""Return true if light is on."""
return self._state
@property
def brightness(self):
"""Return the brightness of this light between 0..255."""
return self._brightness
@property
def supported_features(self):
"""Return the supported features."""
return SUPPORT_BRIGHTNESS
async def _try_command(self, mask_error, func, *args, **kwargs):
"""Call a light command handling error messages."""
try:
result = await self.hass.async_add_executor_job(
partial(func, *args, **kwargs)
)
_LOGGER.debug("Response received from light: %s", result)
return result == SUCCESS
except DeviceException as exc:
if self._available:
_LOGGER.error(mask_error, exc)
self._available = False
return False
async def async_turn_on(self, **kwargs):
"""Turn the light on."""
if ATTR_BRIGHTNESS in kwargs:
brightness = kwargs[ATTR_BRIGHTNESS]
percent_brightness = ceil(100 * brightness / 255.0)
_LOGGER.debug("Setting brightness: %s %s%%", brightness, percent_brightness)
result = await self._try_command(
"Setting brightness failed: %s",
self._device.set_brightness,
percent_brightness,
)
if result:
self._brightness = brightness
else:
await self._try_command("Turning the light on failed.", self._device.on)
async def async_turn_off(self, **kwargs):
"""Turn the light off."""
await self._try_command("Turning the light off failed.", self._device.off)
async def async_update(self):
"""Fetch state from the device."""
try:
state = await self.hass.async_add_executor_job(self._device.status)
except DeviceException as ex:
if self._available:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
return
_LOGGER.debug("Got new state: %s", state)
self._available = True
self._state = state.is_on
self._brightness = ceil((255 / 100.0) * state.brightness)
class XiaomiPhilipsGenericLight(XiaomiPhilipsAbstractLight):
"""Representation of a Generic Xiaomi Philips Light."""
def __init__(self, name, device, entry, unique_id):
"""Initialize the light device."""
super().__init__(name, device, entry, unique_id)
self._state_attrs.update({ATTR_SCENE: None, ATTR_DELAYED_TURN_OFF: None})
async def async_update(self):
"""Fetch state from the device."""
try:
state = await self.hass.async_add_executor_job(self._device.status)
except DeviceException as ex:
if self._available:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
return
_LOGGER.debug("Got new state: %s", state)
self._available = True
self._state = state.is_on
self._brightness = ceil((255 / 100.0) * state.brightness)
delayed_turn_off = self.delayed_turn_off_timestamp(
state.delay_off_countdown,
dt.utcnow(),
self._state_attrs[ATTR_DELAYED_TURN_OFF],
)
self._state_attrs.update(
{ATTR_SCENE: state.scene, ATTR_DELAYED_TURN_OFF: delayed_turn_off}
)
async def async_set_scene(self, scene: int = 1):
"""Set the fixed scene."""
await self._try_command(
"Setting a fixed scene failed.", self._device.set_scene, scene
)
async def async_set_delayed_turn_off(self, time_period: timedelta):
"""Set delayed turn off."""
await self._try_command(
"Setting the turn off delay failed.",
self._device.delay_off,
time_period.total_seconds(),
)
@staticmethod
def delayed_turn_off_timestamp(
countdown: int, current: datetime, previous: datetime
):
"""Update the turn off timestamp only if necessary."""
if countdown is not None and countdown > 0:
new = current.replace(microsecond=0) + timedelta(seconds=countdown)
if previous is None:
return new
lower = timedelta(seconds=-DELAYED_TURN_OFF_MAX_DEVIATION_SECONDS)
upper = timedelta(seconds=DELAYED_TURN_OFF_MAX_DEVIATION_SECONDS)
diff = previous - new
if lower < diff < upper:
return previous
return new
return None
class XiaomiPhilipsBulb(XiaomiPhilipsGenericLight):
"""Representation of a Xiaomi Philips Bulb."""
def __init__(self, name, device, entry, unique_id):
"""Initialize the light device."""
super().__init__(name, device, entry, unique_id)
self._color_temp = None
@property
def color_temp(self):
"""Return the color temperature."""
return self._color_temp
@property
def min_mireds(self):
"""Return the coldest color_temp that this light supports."""
return 175
@property
def max_mireds(self):
"""Return the warmest color_temp that this light supports."""
return 333
@property
def supported_features(self):
"""Return the supported features."""
return SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP
async def async_turn_on(self, **kwargs):
"""Turn the light on."""
if ATTR_COLOR_TEMP in kwargs:
color_temp = kwargs[ATTR_COLOR_TEMP]
percent_color_temp = self.translate(
color_temp, self.max_mireds, self.min_mireds, CCT_MIN, CCT_MAX
)
if ATTR_BRIGHTNESS in kwargs:
brightness = kwargs[ATTR_BRIGHTNESS]
percent_brightness = ceil(100 * brightness / 255.0)
if ATTR_BRIGHTNESS in kwargs and ATTR_COLOR_TEMP in kwargs:
_LOGGER.debug(
"Setting brightness and color temperature: "
"%s %s%%, %s mireds, %s%% cct",
brightness,
percent_brightness,
color_temp,
percent_color_temp,
)
result = await self._try_command(
"Setting brightness and color temperature failed: %s bri, %s cct",
self._device.set_brightness_and_color_temperature,
percent_brightness,
percent_color_temp,
)
if result:
self._color_temp = color_temp
self._brightness = brightness
elif ATTR_COLOR_TEMP in kwargs:
_LOGGER.debug(
"Setting color temperature: %s mireds, %s%% cct",
color_temp,
percent_color_temp,
)
result = await self._try_command(
"Setting color temperature failed: %s cct",
self._device.set_color_temperature,
percent_color_temp,
)
if result:
self._color_temp = color_temp
elif ATTR_BRIGHTNESS in kwargs:
brightness = kwargs[ATTR_BRIGHTNESS]
percent_brightness = ceil(100 * brightness / 255.0)
_LOGGER.debug("Setting brightness: %s %s%%", brightness, percent_brightness)
result = await self._try_command(
"Setting brightness failed: %s",
self._device.set_brightness,
percent_brightness,
)
if result:
self._brightness = brightness
else:
await self._try_command("Turning the light on failed.", self._device.on)
async def async_update(self):
"""Fetch state from the device."""
try:
state = await self.hass.async_add_executor_job(self._device.status)
except DeviceException as ex:
if self._available:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
return
_LOGGER.debug("Got new state: %s", state)
self._available = True
self._state = state.is_on
self._brightness = ceil((255 / 100.0) * state.brightness)
self._color_temp = self.translate(
state.color_temperature, CCT_MIN, CCT_MAX, self.max_mireds, self.min_mireds
)
delayed_turn_off = self.delayed_turn_off_timestamp(
state.delay_off_countdown,
dt.utcnow(),
self._state_attrs[ATTR_DELAYED_TURN_OFF],
)
self._state_attrs.update(
{ATTR_SCENE: state.scene, ATTR_DELAYED_TURN_OFF: delayed_turn_off}
)
@staticmethod
def translate(value, left_min, left_max, right_min, right_max):
"""Map a value from left span to right span."""
left_span = left_max - left_min
right_span = right_max - right_min
value_scaled = float(value - left_min) / float(left_span)
return int(right_min + (value_scaled * right_span))
class XiaomiPhilipsCeilingLamp(XiaomiPhilipsBulb):
"""Representation of a Xiaomi Philips Ceiling Lamp."""
def __init__(self, name, device, entry, unique_id):
"""Initialize the light device."""
super().__init__(name, device, entry, unique_id)
self._state_attrs.update(
{ATTR_NIGHT_LIGHT_MODE: None, ATTR_AUTOMATIC_COLOR_TEMPERATURE: None}
)
@property
def min_mireds(self):
"""Return the coldest color_temp that this light supports."""
return 175
@property
def max_mireds(self):
"""Return the warmest color_temp that this light supports."""
return 370
async def async_update(self):
"""Fetch state from the device."""
try:
state = await self.hass.async_add_executor_job(self._device.status)
except DeviceException as ex:
if self._available:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
return
_LOGGER.debug("Got new state: %s", state)
self._available = True
self._state = state.is_on
self._brightness = ceil((255 / 100.0) * state.brightness)
self._color_temp = self.translate(
state.color_temperature, CCT_MIN, CCT_MAX, self.max_mireds, self.min_mireds
)
delayed_turn_off = self.delayed_turn_off_timestamp(
state.delay_off_countdown,
dt.utcnow(),
self._state_attrs[ATTR_DELAYED_TURN_OFF],
)
self._state_attrs.update(
{
ATTR_SCENE: state.scene,
ATTR_DELAYED_TURN_OFF: delayed_turn_off,
ATTR_NIGHT_LIGHT_MODE: state.smart_night_light,
ATTR_AUTOMATIC_COLOR_TEMPERATURE: state.automatic_color_temperature,
}
)
class XiaomiPhilipsEyecareLamp(XiaomiPhilipsGenericLight):
"""Representation of a Xiaomi Philips Eyecare Lamp 2."""
def __init__(self, name, device, entry, unique_id):
"""Initialize the light device."""
super().__init__(name, device, entry, unique_id)
self._state_attrs.update(
{ATTR_REMINDER: None, ATTR_NIGHT_LIGHT_MODE: None, ATTR_EYECARE_MODE: None}
)
async def async_update(self):
"""Fetch state from the device."""
try:
state = await self.hass.async_add_executor_job(self._device.status)
except DeviceException as ex:
if self._available:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
return
_LOGGER.debug("Got new state: %s", state)
self._available = True
self._state = state.is_on
self._brightness = ceil((255 / 100.0) * state.brightness)
delayed_turn_off = self.delayed_turn_off_timestamp(
state.delay_off_countdown,
dt.utcnow(),
self._state_attrs[ATTR_DELAYED_TURN_OFF],
)
self._state_attrs.update(
{
ATTR_SCENE: state.scene,
ATTR_DELAYED_TURN_OFF: delayed_turn_off,
ATTR_REMINDER: state.reminder,
ATTR_NIGHT_LIGHT_MODE: state.smart_night_light,
ATTR_EYECARE_MODE: state.eyecare,
}
)
async def async_set_delayed_turn_off(self, time_period: timedelta):
"""Set delayed turn off."""
await self._try_command(
"Setting the turn off delay failed.",
self._device.delay_off,
round(time_period.total_seconds() / 60),
)
async def async_reminder_on(self):
"""Enable the eye fatigue notification."""
await self._try_command(
"Turning on the reminder failed.", self._device.reminder_on
)
async def async_reminder_off(self):
"""Disable the eye fatigue notification."""
await self._try_command(
"Turning off the reminder failed.", self._device.reminder_off
)
async def async_night_light_mode_on(self):
"""Turn the smart night light mode on."""
await self._try_command(
"Turning on the smart night light mode failed.",
self._device.smart_night_light_on,
)
async def async_night_light_mode_off(self):
"""Turn the smart night light mode off."""
await self._try_command(
"Turning off the smart night light mode failed.",
self._device.smart_night_light_off,
)
async def async_eyecare_mode_on(self):
"""Turn the eyecare mode on."""
await self._try_command(
"Turning on the eyecare mode failed.", self._device.eyecare_on
)
async def async_eyecare_mode_off(self):
"""Turn the eyecare mode off."""
await self._try_command(
"Turning off the eyecare mode failed.", self._device.eyecare_off
)
@staticmethod
def delayed_turn_off_timestamp(
countdown: int, current: datetime, previous: datetime
):
"""Update the turn off timestamp only if necessary."""
if countdown is not None and countdown > 0:
new = current.replace(second=0, microsecond=0) + timedelta(
minutes=countdown
)
if previous is None:
return new
lower = timedelta(minutes=-DELAYED_TURN_OFF_MAX_DEVIATION_MINUTES)
upper = timedelta(minutes=DELAYED_TURN_OFF_MAX_DEVIATION_MINUTES)
diff = previous - new
if lower < diff < upper:
return previous
return new
return None
class XiaomiPhilipsEyecareLampAmbientLight(XiaomiPhilipsAbstractLight):
"""Representation of a Xiaomi Philips Eyecare Lamp Ambient Light."""
def __init__(self, name, device, entry, unique_id):
"""Initialize the light device."""
name = f"{name} Ambient Light"
if unique_id is not None:
unique_id = f"{unique_id}-ambient"
super().__init__(name, device, entry, unique_id)
async def async_turn_on(self, **kwargs):
"""Turn the light on."""
if ATTR_BRIGHTNESS in kwargs:
brightness = kwargs[ATTR_BRIGHTNESS]
percent_brightness = ceil(100 * brightness / 255.0)
_LOGGER.debug(
"Setting brightness of the ambient light: %s %s%%",
brightness,
percent_brightness,
)
result = await self._try_command(
"Setting brightness of the ambient failed: %s",
self._device.set_ambient_brightness,
percent_brightness,
)
if result:
self._brightness = brightness
else:
await self._try_command(
"Turning the ambient light on failed.", self._device.ambient_on
)
async def async_turn_off(self, **kwargs):
"""Turn the light off."""
await self._try_command(
"Turning the ambient light off failed.", self._device.ambient_off
)
async def async_update(self):
"""Fetch state from the device."""
try:
state = await self.hass.async_add_executor_job(self._device.status)
except DeviceException as ex:
if self._available:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
return
_LOGGER.debug("Got new state: %s", state)
self._available = True
self._state = state.ambient
self._brightness = ceil((255 / 100.0) * state.ambient_brightness)
class XiaomiPhilipsMoonlightLamp(XiaomiPhilipsBulb):
"""Representation of a Xiaomi Philips Zhirui Bedside Lamp."""
def __init__(self, name, device, entry, unique_id):
"""Initialize the light device."""
super().__init__(name, device, entry, unique_id)
self._hs_color = None
self._state_attrs.pop(ATTR_DELAYED_TURN_OFF)
self._state_attrs.update(
{
ATTR_SLEEP_ASSISTANT: None,
ATTR_SLEEP_OFF_TIME: None,
ATTR_TOTAL_ASSISTANT_SLEEP_TIME: None,
ATTR_BAND_SLEEP: None,
ATTR_BAND: None,
}
)
@property
def min_mireds(self):
"""Return the coldest color_temp that this light supports."""
return 153
@property
def max_mireds(self):
"""Return the warmest color_temp that this light supports."""
return 588
@property
def hs_color(self) -> tuple:
"""Return the hs color value."""
return self._hs_color
@property
def supported_features(self):
"""Return the supported features."""
return SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_COLOR_TEMP
async def async_turn_on(self, **kwargs):
"""Turn the light on."""
if ATTR_COLOR_TEMP in kwargs:
color_temp = kwargs[ATTR_COLOR_TEMP]
percent_color_temp = self.translate(
color_temp, self.max_mireds, self.min_mireds, CCT_MIN, CCT_MAX
)
if ATTR_BRIGHTNESS in kwargs:
brightness = kwargs[ATTR_BRIGHTNESS]
percent_brightness = ceil(100 * brightness / 255.0)
if ATTR_HS_COLOR in kwargs:
hs_color = kwargs[ATTR_HS_COLOR]
rgb = color.color_hs_to_RGB(*hs_color)
if ATTR_BRIGHTNESS in kwargs and ATTR_HS_COLOR in kwargs:
_LOGGER.debug(
"Setting brightness and color: %s %s%%, %s",
brightness,
percent_brightness,
rgb,
)
result = await self._try_command(
"Setting brightness and color failed: %s bri, %s color",
self._device.set_brightness_and_rgb,
percent_brightness,
rgb,
)
if result:
self._hs_color = hs_color
self._brightness = brightness
elif ATTR_BRIGHTNESS in kwargs and ATTR_COLOR_TEMP in kwargs:
_LOGGER.debug(
"Setting brightness and color temperature: "
"%s %s%%, %s mireds, %s%% cct",
brightness,
percent_brightness,
color_temp,
percent_color_temp,
)
result = await self._try_command(
"Setting brightness and color temperature failed: %s bri, %s cct",
self._device.set_brightness_and_color_temperature,
percent_brightness,
percent_color_temp,
)
if result:
self._color_temp = color_temp
self._brightness = brightness
elif ATTR_HS_COLOR in kwargs:
_LOGGER.debug("Setting color: %s", rgb)
result = await self._try_command(
"Setting color failed: %s", self._device.set_rgb, rgb
)
if result:
self._hs_color = hs_color
elif ATTR_COLOR_TEMP in kwargs:
_LOGGER.debug(
"Setting color temperature: %s mireds, %s%% cct",
color_temp,
percent_color_temp,
)
result = await self._try_command(
"Setting color temperature failed: %s cct",
self._device.set_color_temperature,
percent_color_temp,
)
if result:
self._color_temp = color_temp
elif ATTR_BRIGHTNESS in kwargs:
brightness = kwargs[ATTR_BRIGHTNESS]
percent_brightness = ceil(100 * brightness / 255.0)
_LOGGER.debug("Setting brightness: %s %s%%", brightness, percent_brightness)
result = await self._try_command(
"Setting brightness failed: %s",
self._device.set_brightness,
percent_brightness,
)
if result:
self._brightness = brightness
else:
await self._try_command("Turning the light on failed.", self._device.on)
async def async_update(self):
"""Fetch state from the device."""
try:
state = await self.hass.async_add_executor_job(self._device.status)
except DeviceException as ex:
if self._available:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
return
_LOGGER.debug("Got new state: %s", state)
self._available = True
self._state = state.is_on
self._brightness = ceil((255 / 100.0) * state.brightness)
self._color_temp = self.translate(
state.color_temperature, CCT_MIN, CCT_MAX, self.max_mireds, self.min_mireds
)
self._hs_color = color.color_RGB_to_hs(*state.rgb)
self._state_attrs.update(
{
ATTR_SCENE: state.scene,
ATTR_SLEEP_ASSISTANT: state.sleep_assistant,
ATTR_SLEEP_OFF_TIME: state.sleep_off_time,
ATTR_TOTAL_ASSISTANT_SLEEP_TIME: state.total_assistant_sleep_time,
ATTR_BAND_SLEEP: state.brand_sleep,
ATTR_BAND: state.brand,
}
)
async def async_set_delayed_turn_off(self, time_period: timedelta):
"""Set delayed turn off. Unsupported."""
return
class XiaomiGatewayLight(LightEntity):
"""Representation of a gateway device's light."""
def __init__(self, gateway_device, gateway_name, gateway_device_id):
"""Initialize the XiaomiGatewayLight."""
self._gateway = gateway_device
self._name = f"{gateway_name} Light"
self._gateway_device_id = gateway_device_id
self._unique_id = gateway_device_id
self._available = False
self._is_on = None
self._brightness_pct = 100
self._rgb = (255, 255, 255)
self._hs = (0, 0)
@property
def unique_id(self):
"""Return an unique ID."""
return self._unique_id
@property
def device_info(self) -> DeviceInfo:
"""Return the device info of the gateway."""
return DeviceInfo(
identifiers={(DOMAIN, self._gateway_device_id)},
)
@property
def name(self):
"""Return the name of this entity, if any."""
return self._name
@property
def available(self):
"""Return true when state is known."""
return self._available
@property
def is_on(self):
"""Return true if it is on."""
return self._is_on
@property
def brightness(self):
"""Return the brightness of this light between 0..255."""
return int(255 * self._brightness_pct / 100)
@property
def hs_color(self):
"""Return the hs color value."""
return self._hs
@property
def supported_features(self):
"""Return the supported features."""
return SUPPORT_BRIGHTNESS | SUPPORT_COLOR
def turn_on(self, **kwargs):
"""Turn the light on."""
if ATTR_HS_COLOR in kwargs:
rgb = color.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR])
else:
rgb = self._rgb
if ATTR_BRIGHTNESS in kwargs:
brightness_pct = int(100 * kwargs[ATTR_BRIGHTNESS] / 255)
else:
brightness_pct = self._brightness_pct
self._gateway.light.set_rgb(brightness_pct, rgb)
self.schedule_update_ha_state()
def turn_off(self, **kwargs):
"""Turn the light off."""
self._gateway.light.set_rgb(0, self._rgb)
self.schedule_update_ha_state()
async def async_update(self):
"""Fetch state from the device."""
try:
state_dict = await self.hass.async_add_executor_job(
self._gateway.light.rgb_status
)
except GatewayException as ex:
if self._available:
self._available = False
_LOGGER.error(
"Got exception while fetching the gateway light state: %s", ex
)
return
self._available = True
self._is_on = state_dict["is_on"]
if self._is_on:
self._brightness_pct = state_dict["brightness"]
self._rgb = state_dict["rgb"]
self._hs = color.color_RGB_to_hs(*self._rgb)
class XiaomiGatewayBulb(XiaomiGatewayDevice, LightEntity):
"""Representation of Xiaomi Gateway Bulb."""
@property
def brightness(self):
"""Return the brightness of the light."""
return round((self._sub_device.status["brightness"] * 255) / 100)
@property
def color_temp(self):
"""Return current color temperature."""
return self._sub_device.status["color_temp"]
@property
def is_on(self):
"""Return true if light is on."""
return self._sub_device.status["status"] == "on"
@property
def min_mireds(self):
"""Return min cct."""
return self._sub_device.status["cct_min"]
@property
def max_mireds(self):
"""Return max cct."""
return self._sub_device.status["cct_max"]
@property
def supported_features(self):
"""Return the supported features."""
return SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP
async def async_turn_on(self, **kwargs):
"""Instruct the light to turn on."""
await self.hass.async_add_executor_job(self._sub_device.on)
if ATTR_COLOR_TEMP in kwargs:
color_temp = kwargs[ATTR_COLOR_TEMP]
await self.hass.async_add_executor_job(
self._sub_device.set_color_temp, color_temp
)
if ATTR_BRIGHTNESS in kwargs:
brightness = round((kwargs[ATTR_BRIGHTNESS] * 100) / 255)
await self.hass.async_add_executor_job(
self._sub_device.set_brightness, brightness
)
async def async_turn_off(self, **kwargsf):
"""Instruct the light to turn off."""
await self.hass.async_add_executor_job(self._sub_device.off)