Home Assistant Unofficial Reference 2024.12.1
light.py
Go to the documentation of this file.
1 """Support for Xiaomi Philips Lights."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 import datetime
7 from datetime import timedelta
8 from functools import partial
9 import logging
10 from math import ceil
11 from typing import Any
12 
13 from miio import (
14  Ceil,
15  Device as MiioDevice,
16  DeviceException,
17  PhilipsBulb,
18  PhilipsEyecare,
19  PhilipsMoonlight,
20 )
21 from miio.gateway.gateway import (
22  GATEWAY_MODEL_AC_V1,
23  GATEWAY_MODEL_AC_V2,
24  GATEWAY_MODEL_AC_V3,
25  GatewayException,
26 )
27 import voluptuous as vol
28 
30  ATTR_BRIGHTNESS,
31  ATTR_COLOR_TEMP,
32  ATTR_HS_COLOR,
33  ColorMode,
34  LightEntity,
35 )
36 from homeassistant.config_entries import ConfigEntry
37 from homeassistant.const import (
38  ATTR_ENTITY_ID,
39  CONF_DEVICE,
40  CONF_HOST,
41  CONF_MODEL,
42  CONF_TOKEN,
43 )
44 from homeassistant.core import HomeAssistant, ServiceCall
46 from homeassistant.helpers.device_registry import DeviceInfo
47 from homeassistant.helpers.entity_platform import AddEntitiesCallback
48 from homeassistant.util import color, dt as dt_util
49 
50 from .const import (
51  CONF_FLOW_TYPE,
52  CONF_GATEWAY,
53  DOMAIN,
54  KEY_COORDINATOR,
55  MODELS_LIGHT_BULB,
56  MODELS_LIGHT_CEILING,
57  MODELS_LIGHT_EYECARE,
58  MODELS_LIGHT_MONO,
59  MODELS_LIGHT_MOON,
60  SERVICE_EYECARE_MODE_OFF,
61  SERVICE_EYECARE_MODE_ON,
62  SERVICE_NIGHT_LIGHT_MODE_OFF,
63  SERVICE_NIGHT_LIGHT_MODE_ON,
64  SERVICE_REMINDER_OFF,
65  SERVICE_REMINDER_ON,
66  SERVICE_SET_DELAYED_TURN_OFF,
67  SERVICE_SET_SCENE,
68 )
69 from .entity import XiaomiGatewayDevice, XiaomiMiioEntity
70 from .typing import ServiceMethodDetails
71 
72 _LOGGER = logging.getLogger(__name__)
73 
74 DEFAULT_NAME = "Xiaomi Philips Light"
75 DATA_KEY = "light.xiaomi_miio"
76 
77 # The light does not accept cct values < 1
78 CCT_MIN = 1
79 CCT_MAX = 100
80 
81 DELAYED_TURN_OFF_MAX_DEVIATION_SECONDS = 4
82 DELAYED_TURN_OFF_MAX_DEVIATION_MINUTES = 1
83 
84 SUCCESS = ["ok"]
85 ATTR_SCENE = "scene"
86 ATTR_DELAYED_TURN_OFF = "delayed_turn_off"
87 ATTR_TIME_PERIOD = "time_period"
88 ATTR_NIGHT_LIGHT_MODE = "night_light_mode"
89 ATTR_AUTOMATIC_COLOR_TEMPERATURE = "automatic_color_temperature"
90 ATTR_REMINDER = "reminder"
91 ATTR_EYECARE_MODE = "eyecare_mode"
92 
93 # Moonlight
94 ATTR_SLEEP_ASSISTANT = "sleep_assistant"
95 ATTR_SLEEP_OFF_TIME = "sleep_off_time"
96 ATTR_TOTAL_ASSISTANT_SLEEP_TIME = "total_assistant_sleep_time"
97 ATTR_BAND_SLEEP = "band_sleep"
98 ATTR_BAND = "band"
99 
100 XIAOMI_MIIO_SERVICE_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids})
101 
102 SERVICE_SCHEMA_SET_SCENE = XIAOMI_MIIO_SERVICE_SCHEMA.extend(
103  {vol.Required(ATTR_SCENE): vol.All(vol.Coerce(int), vol.Clamp(min=1, max=6))}
104 )
105 
106 SERVICE_SCHEMA_SET_DELAYED_TURN_OFF = XIAOMI_MIIO_SERVICE_SCHEMA.extend(
107  {vol.Required(ATTR_TIME_PERIOD): cv.positive_time_period}
108 )
109 
110 SERVICE_TO_METHOD = {
111  SERVICE_SET_DELAYED_TURN_OFF: ServiceMethodDetails(
112  method="async_set_delayed_turn_off",
113  schema=SERVICE_SCHEMA_SET_DELAYED_TURN_OFF,
114  ),
115  SERVICE_SET_SCENE: ServiceMethodDetails(
116  method="async_set_scene",
117  schema=SERVICE_SCHEMA_SET_SCENE,
118  ),
119  SERVICE_REMINDER_ON: ServiceMethodDetails(method="async_reminder_on"),
120  SERVICE_REMINDER_OFF: ServiceMethodDetails(method="async_reminder_off"),
121  SERVICE_NIGHT_LIGHT_MODE_ON: ServiceMethodDetails(
122  method="async_night_light_mode_on"
123  ),
124  SERVICE_NIGHT_LIGHT_MODE_OFF: ServiceMethodDetails(
125  method="async_night_light_mode_off"
126  ),
127  SERVICE_EYECARE_MODE_ON: ServiceMethodDetails(method="async_eyecare_mode_on"),
128  SERVICE_EYECARE_MODE_OFF: ServiceMethodDetails(method="async_eyecare_mode_off"),
129 }
130 
131 
133  hass: HomeAssistant,
134  config_entry: ConfigEntry,
135  async_add_entities: AddEntitiesCallback,
136 ) -> None:
137  """Set up the Xiaomi light from a config entry."""
138  entities: list[LightEntity] = []
139  entity: LightEntity
140  light: MiioDevice
141 
142  if config_entry.data[CONF_FLOW_TYPE] == CONF_GATEWAY:
143  gateway = hass.data[DOMAIN][config_entry.entry_id][CONF_GATEWAY]
144  # Gateway light
145  if gateway.model not in [
146  GATEWAY_MODEL_AC_V1,
147  GATEWAY_MODEL_AC_V2,
148  GATEWAY_MODEL_AC_V3,
149  ]:
150  entities.append(
151  XiaomiGatewayLight(gateway, config_entry.title, config_entry.unique_id)
152  )
153  # Gateway sub devices
154  sub_devices = gateway.devices
155  for sub_device in sub_devices.values():
156  if sub_device.device_type == "LightBulb":
157  coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR][
158  sub_device.sid
159  ]
160  entities.append(
161  XiaomiGatewayBulb(coordinator, sub_device, config_entry)
162  )
163 
164  if config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE:
165  if DATA_KEY not in hass.data:
166  hass.data[DATA_KEY] = {}
167 
168  host = config_entry.data[CONF_HOST]
169  token = config_entry.data[CONF_TOKEN]
170  name = config_entry.title
171  model = config_entry.data[CONF_MODEL]
172  unique_id = config_entry.unique_id
173 
174  _LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5])
175 
176  if model in MODELS_LIGHT_EYECARE:
177  light = PhilipsEyecare(host, token)
178  entity = XiaomiPhilipsEyecareLamp(name, light, config_entry, unique_id)
179  entities.append(entity)
180  hass.data[DATA_KEY][host] = entity
181 
182  entities.append(
184  name, light, config_entry, unique_id
185  )
186  )
187  # The ambient light doesn't expose additional services.
188  # A hass.data[DATA_KEY] entry isn't needed.
189  elif model in MODELS_LIGHT_CEILING:
190  light = Ceil(host, token)
191  entity = XiaomiPhilipsCeilingLamp(name, light, config_entry, unique_id)
192  entities.append(entity)
193  hass.data[DATA_KEY][host] = entity
194  elif model in MODELS_LIGHT_MOON:
195  light = PhilipsMoonlight(host, token)
196  entity = XiaomiPhilipsMoonlightLamp(name, light, config_entry, unique_id)
197  entities.append(entity)
198  hass.data[DATA_KEY][host] = entity
199  elif model in MODELS_LIGHT_BULB:
200  light = PhilipsBulb(host, token)
201  entity = XiaomiPhilipsBulb(name, light, config_entry, unique_id)
202  entities.append(entity)
203  hass.data[DATA_KEY][host] = entity
204  elif model in MODELS_LIGHT_MONO:
205  light = PhilipsBulb(host, token)
206  entity = XiaomiPhilipsGenericLight(name, light, config_entry, unique_id)
207  entities.append(entity)
208  hass.data[DATA_KEY][host] = entity
209  else:
210  _LOGGER.error(
211  (
212  "Unsupported device found! Please create an issue at "
213  "https://github.com/syssi/philipslight/issues "
214  "and provide the following data: %s"
215  ),
216  model,
217  )
218  return
219 
220  async def async_service_handler(service: ServiceCall) -> None:
221  """Map services to methods on Xiaomi Philips Lights."""
222  method = SERVICE_TO_METHOD[service.service]
223  params = {
224  key: value
225  for key, value in service.data.items()
226  if key != ATTR_ENTITY_ID
227  }
228  if entity_ids := service.data.get(ATTR_ENTITY_ID):
229  target_devices = [
230  dev
231  for dev in hass.data[DATA_KEY].values()
232  if dev.entity_id in entity_ids
233  ]
234  else:
235  target_devices = hass.data[DATA_KEY].values()
236 
237  update_tasks = []
238  for target_device in target_devices:
239  if not hasattr(target_device, method.method):
240  continue
241  await getattr(target_device, method.method)(**params)
242  update_tasks.append(
243  asyncio.create_task(target_device.async_update_ha_state(True))
244  )
245 
246  if update_tasks:
247  await asyncio.wait(update_tasks)
248 
249  for xiaomi_miio_service, method in SERVICE_TO_METHOD.items():
250  schema = method.schema or XIAOMI_MIIO_SERVICE_SCHEMA
251  hass.services.async_register(
252  DOMAIN, xiaomi_miio_service, async_service_handler, schema=schema
253  )
254 
255  async_add_entities(entities, update_before_add=True)
256 
257 
259  """Representation of a Abstract Xiaomi Philips Light."""
260 
261  _attr_color_mode = ColorMode.BRIGHTNESS
262  _attr_supported_color_modes = {ColorMode.BRIGHTNESS}
263 
264  def __init__(self, name, device, entry, unique_id):
265  """Initialize the light device."""
266  super().__init__(name, device, entry, unique_id)
267 
268  self._brightness_brightness = None
269  self._available_available_available = False
270  self._state_state = None
271  self._state_attrs_state_attrs = {}
272 
273  @property
274  def available(self):
275  """Return true when state is known."""
276  return self._available_available_available
277 
278  @property
280  """Return the state attributes of the device."""
281  return self._state_attrs_state_attrs
282 
283  @property
284  def is_on(self):
285  """Return true if light is on."""
286  return self._state_state
287 
288  @property
289  def brightness(self):
290  """Return the brightness of this light between 0..255."""
291  return self._brightness_brightness
292 
293  async def _try_command(self, mask_error, func, *args, **kwargs):
294  """Call a light command handling error messages."""
295  try:
296  result = await self.hasshass.async_add_executor_job(
297  partial(func, *args, **kwargs)
298  )
299  except DeviceException as exc:
300  if self._available_available_available:
301  _LOGGER.error(mask_error, exc)
302  self._available_available_available = False
303 
304  return False
305 
306  _LOGGER.debug("Response received from light: %s", result)
307  return result == SUCCESS
308 
309  async def async_turn_on(self, **kwargs: Any) -> None:
310  """Turn the light on."""
311  if ATTR_BRIGHTNESS in kwargs:
312  brightness = kwargs[ATTR_BRIGHTNESS]
313  percent_brightness = ceil(100 * brightness / 255.0)
314 
315  _LOGGER.debug("Setting brightness: %s %s%%", brightness, percent_brightness)
316 
317  result = await self._try_command_try_command(
318  "Setting brightness failed: %s",
319  self._device_device.set_brightness,
320  percent_brightness,
321  )
322 
323  if result:
324  self._brightness_brightness = brightness
325  else:
326  await self._try_command_try_command("Turning the light on failed.", self._device_device.on)
327 
328  async def async_turn_off(self, **kwargs: Any) -> None:
329  """Turn the light off."""
330  await self._try_command_try_command("Turning the light off failed.", self._device_device.off)
331 
332  async def async_update(self) -> None:
333  """Fetch state from the device."""
334  try:
335  state = await self.hasshass.async_add_executor_job(self._device_device.status)
336  except DeviceException as ex:
337  if self._available_available_available:
338  self._available_available_available = False
339  _LOGGER.error("Got exception while fetching the state: %s", ex)
340 
341  return
342 
343  _LOGGER.debug("Got new state: %s", state)
344  self._available_available_available = True
345  self._state_state = state.is_on
346  self._brightness_brightness = ceil((255 / 100.0) * state.brightness)
347 
348 
350  """Representation of a Generic Xiaomi Philips Light."""
351 
352  def __init__(self, name, device, entry, unique_id):
353  """Initialize the light device."""
354  super().__init__(name, device, entry, unique_id)
355 
356  self._state_attrs_state_attrs.update({ATTR_SCENE: None, ATTR_DELAYED_TURN_OFF: None})
357 
358  async def async_update(self) -> None:
359  """Fetch state from the device."""
360  try:
361  state = await self.hasshass.async_add_executor_job(self._device_device.status)
362  except DeviceException as ex:
363  if self._available_available_available_available:
364  self._available_available_available_available = False
365  _LOGGER.error("Got exception while fetching the state: %s", ex)
366 
367  return
368 
369  _LOGGER.debug("Got new state: %s", state)
370  self._available_available_available_available = True
371  self._state_state_state = state.is_on
372  self._brightness_brightness_brightness = ceil((255 / 100.0) * state.brightness)
373 
374  delayed_turn_off = self.delayed_turn_off_timestampdelayed_turn_off_timestamp(
375  state.delay_off_countdown,
376  dt_util.utcnow(),
377  self._state_attrs_state_attrs[ATTR_DELAYED_TURN_OFF],
378  )
379 
380  self._state_attrs_state_attrs.update(
381  {ATTR_SCENE: state.scene, ATTR_DELAYED_TURN_OFF: delayed_turn_off}
382  )
383 
384  async def async_set_scene(self, scene: int = 1):
385  """Set the fixed scene."""
386  await self._try_command_try_command(
387  "Setting a fixed scene failed.", self._device_device.set_scene, scene
388  )
389 
390  async def async_set_delayed_turn_off(self, time_period: timedelta):
391  """Set delayed turn off."""
392  await self._try_command_try_command(
393  "Setting the turn off delay failed.",
394  self._device_device.delay_off,
395  time_period.total_seconds(),
396  )
397 
398  @staticmethod
400  countdown: int, current: datetime.datetime, previous: datetime.datetime
401  ):
402  """Update the turn off timestamp only if necessary."""
403  if countdown is not None and countdown > 0:
404  new = current.replace(microsecond=0) + timedelta(seconds=countdown)
405 
406  if previous is None:
407  return new
408 
409  lower = timedelta(seconds=-DELAYED_TURN_OFF_MAX_DEVIATION_SECONDS)
410  upper = timedelta(seconds=DELAYED_TURN_OFF_MAX_DEVIATION_SECONDS)
411  diff = previous - new
412  if lower < diff < upper:
413  return previous
414 
415  return new
416 
417  return None
418 
419 
421  """Representation of a Xiaomi Philips Bulb."""
422 
423  _attr_color_mode = ColorMode.COLOR_TEMP
424  _attr_supported_color_modes = {ColorMode.COLOR_TEMP}
425 
426  def __init__(self, name, device, entry, unique_id):
427  """Initialize the light device."""
428  super().__init__(name, device, entry, unique_id)
429 
430  self._color_temp_color_temp = None
431 
432  @property
433  def color_temp(self):
434  """Return the color temperature."""
435  return self._color_temp_color_temp
436 
437  @property
438  def min_mireds(self):
439  """Return the coldest color_temp that this light supports."""
440  return 175
441 
442  @property
443  def max_mireds(self):
444  """Return the warmest color_temp that this light supports."""
445  return 333
446 
447  async def async_turn_on(self, **kwargs: Any) -> None:
448  """Turn the light on."""
449  if ATTR_COLOR_TEMP in kwargs:
450  color_temp = kwargs[ATTR_COLOR_TEMP]
451  percent_color_temp = self.translatetranslate(
452  color_temp, self.max_miredsmax_miredsmax_mireds, self.min_miredsmin_miredsmin_mireds, CCT_MIN, CCT_MAX
453  )
454 
455  if ATTR_BRIGHTNESS in kwargs:
456  brightness = kwargs[ATTR_BRIGHTNESS]
457  percent_brightness = ceil(100 * brightness / 255.0)
458 
459  if ATTR_BRIGHTNESS in kwargs and ATTR_COLOR_TEMP in kwargs:
460  _LOGGER.debug(
461  "Setting brightness and color temperature: %s %s%%, %s mireds, %s%% cct",
462  brightness,
463  percent_brightness,
464  color_temp,
465  percent_color_temp,
466  )
467 
468  result = await self._try_command_try_command(
469  "Setting brightness and color temperature failed: %s bri, %s cct",
470  self._device_device.set_brightness_and_color_temperature,
471  percent_brightness,
472  percent_color_temp,
473  )
474 
475  if result:
476  self._color_temp_color_temp = color_temp
477  self._brightness_brightness_brightness_brightness = brightness
478 
479  elif ATTR_COLOR_TEMP in kwargs:
480  _LOGGER.debug(
481  "Setting color temperature: %s mireds, %s%% cct",
482  color_temp,
483  percent_color_temp,
484  )
485 
486  result = await self._try_command_try_command(
487  "Setting color temperature failed: %s cct",
488  self._device_device.set_color_temperature,
489  percent_color_temp,
490  )
491 
492  if result:
493  self._color_temp_color_temp = color_temp
494 
495  elif ATTR_BRIGHTNESS in kwargs:
496  brightness = kwargs[ATTR_BRIGHTNESS]
497  percent_brightness = ceil(100 * brightness / 255.0)
498 
499  _LOGGER.debug("Setting brightness: %s %s%%", brightness, percent_brightness)
500 
501  result = await self._try_command_try_command(
502  "Setting brightness failed: %s",
503  self._device_device.set_brightness,
504  percent_brightness,
505  )
506 
507  if result:
508  self._brightness_brightness_brightness_brightness = brightness
509 
510  else:
511  await self._try_command_try_command("Turning the light on failed.", self._device_device.on)
512 
513  async def async_update(self) -> None:
514  """Fetch state from the device."""
515  try:
516  state = await self.hasshass.async_add_executor_job(self._device_device.status)
517  except DeviceException as ex:
520  _LOGGER.error("Got exception while fetching the state: %s", ex)
521 
522  return
523 
524  _LOGGER.debug("Got new state: %s", state)
525  self._available_available_available_available_available = True
526  self._state_state_state_state = state.is_on
527  self._brightness_brightness_brightness_brightness = ceil((255 / 100.0) * state.brightness)
528  self._color_temp_color_temp = self.translatetranslate(
529  state.color_temperature, CCT_MIN, CCT_MAX, self.max_miredsmax_miredsmax_mireds, self.min_miredsmin_miredsmin_mireds
530  )
531 
532  delayed_turn_off = self.delayed_turn_off_timestampdelayed_turn_off_timestamp(
533  state.delay_off_countdown,
534  dt_util.utcnow(),
535  self._state_attrs_state_attrs[ATTR_DELAYED_TURN_OFF],
536  )
537 
538  self._state_attrs_state_attrs.update(
539  {ATTR_SCENE: state.scene, ATTR_DELAYED_TURN_OFF: delayed_turn_off}
540  )
541 
542  @staticmethod
543  def translate(value, left_min, left_max, right_min, right_max):
544  """Map a value from left span to right span."""
545  left_span = left_max - left_min
546  right_span = right_max - right_min
547  value_scaled = float(value - left_min) / float(left_span)
548  return int(right_min + (value_scaled * right_span))
549 
550 
552  """Representation of a Xiaomi Philips Ceiling Lamp."""
553 
554  def __init__(self, name, device, entry, unique_id):
555  """Initialize the light device."""
556  super().__init__(name, device, entry, unique_id)
557 
558  self._state_attrs_state_attrs.update(
559  {ATTR_NIGHT_LIGHT_MODE: None, ATTR_AUTOMATIC_COLOR_TEMPERATURE: None}
560  )
561 
562  @property
563  def min_mireds(self):
564  """Return the coldest color_temp that this light supports."""
565  return 175
566 
567  @property
568  def max_mireds(self):
569  """Return the warmest color_temp that this light supports."""
570  return 370
571 
572  async def async_update(self) -> None:
573  """Fetch state from the device."""
574  try:
575  state = await self.hasshass.async_add_executor_job(self._device_device.status)
576  except DeviceException as ex:
579  _LOGGER.error("Got exception while fetching the state: %s", ex)
580 
581  return
582 
583  _LOGGER.debug("Got new state: %s", state)
585  self._state_state_state_state_state = state.is_on
586  self._brightness_brightness_brightness_brightness_brightness = ceil((255 / 100.0) * state.brightness)
587  self._color_temp_color_temp_color_temp = self.translatetranslate(
588  state.color_temperature, CCT_MIN, CCT_MAX, self.max_miredsmax_miredsmax_miredsmax_mireds, self.min_miredsmin_miredsmin_miredsmin_mireds
589  )
590 
591  delayed_turn_off = self.delayed_turn_off_timestampdelayed_turn_off_timestamp(
592  state.delay_off_countdown,
593  dt_util.utcnow(),
594  self._state_attrs_state_attrs[ATTR_DELAYED_TURN_OFF],
595  )
596 
597  self._state_attrs_state_attrs.update(
598  {
599  ATTR_SCENE: state.scene,
600  ATTR_DELAYED_TURN_OFF: delayed_turn_off,
601  ATTR_NIGHT_LIGHT_MODE: state.smart_night_light,
602  ATTR_AUTOMATIC_COLOR_TEMPERATURE: state.automatic_color_temperature,
603  }
604  )
605 
606 
608  """Representation of a Xiaomi Philips Eyecare Lamp 2."""
609 
610  def __init__(self, name, device, entry, unique_id):
611  """Initialize the light device."""
612  super().__init__(name, device, entry, unique_id)
613 
614  self._state_attrs_state_attrs.update(
615  {ATTR_REMINDER: None, ATTR_NIGHT_LIGHT_MODE: None, ATTR_EYECARE_MODE: None}
616  )
617 
618  async def async_update(self) -> None:
619  """Fetch state from the device."""
620  try:
621  state = await self.hasshass.async_add_executor_job(self._device_device.status)
622  except DeviceException as ex:
625  _LOGGER.error("Got exception while fetching the state: %s", ex)
626 
627  return
628 
629  _LOGGER.debug("Got new state: %s", state)
630  self._available_available_available_available_available = True
631  self._state_state_state_state = state.is_on
632  self._brightness_brightness_brightness_brightness = ceil((255 / 100.0) * state.brightness)
633 
634  delayed_turn_off = self.delayed_turn_off_timestampdelayed_turn_off_timestampdelayed_turn_off_timestamp(
635  state.delay_off_countdown,
636  dt_util.utcnow(),
637  self._state_attrs_state_attrs[ATTR_DELAYED_TURN_OFF],
638  )
639 
640  self._state_attrs_state_attrs.update(
641  {
642  ATTR_SCENE: state.scene,
643  ATTR_DELAYED_TURN_OFF: delayed_turn_off,
644  ATTR_REMINDER: state.reminder,
645  ATTR_NIGHT_LIGHT_MODE: state.smart_night_light,
646  ATTR_EYECARE_MODE: state.eyecare,
647  }
648  )
649 
650  async def async_set_delayed_turn_off(self, time_period: timedelta):
651  """Set delayed turn off."""
652  await self._try_command_try_command(
653  "Setting the turn off delay failed.",
654  self._device_device.delay_off,
655  round(time_period.total_seconds() / 60),
656  )
657 
658  async def async_reminder_on(self):
659  """Enable the eye fatigue notification."""
660  await self._try_command_try_command(
661  "Turning on the reminder failed.", self._device_device.reminder_on
662  )
663 
664  async def async_reminder_off(self):
665  """Disable the eye fatigue notification."""
666  await self._try_command_try_command(
667  "Turning off the reminder failed.", self._device_device.reminder_off
668  )
669 
670  async def async_night_light_mode_on(self):
671  """Turn the smart night light mode on."""
672  await self._try_command_try_command(
673  "Turning on the smart night light mode failed.",
674  self._device_device.smart_night_light_on,
675  )
676 
677  async def async_night_light_mode_off(self):
678  """Turn the smart night light mode off."""
679  await self._try_command_try_command(
680  "Turning off the smart night light mode failed.",
681  self._device_device.smart_night_light_off,
682  )
683 
684  async def async_eyecare_mode_on(self):
685  """Turn the eyecare mode on."""
686  await self._try_command_try_command(
687  "Turning on the eyecare mode failed.", self._device_device.eyecare_on
688  )
689 
690  async def async_eyecare_mode_off(self):
691  """Turn the eyecare mode off."""
692  await self._try_command_try_command(
693  "Turning off the eyecare mode failed.", self._device_device.eyecare_off
694  )
695 
696  @staticmethod
698  countdown: int, current: datetime.datetime, previous: datetime.datetime
699  ):
700  """Update the turn off timestamp only if necessary."""
701  if countdown is not None and countdown > 0:
702  new = current.replace(second=0, microsecond=0) + timedelta(
703  minutes=countdown
704  )
705 
706  if previous is None:
707  return new
708 
709  lower = timedelta(minutes=-DELAYED_TURN_OFF_MAX_DEVIATION_MINUTES)
710  upper = timedelta(minutes=DELAYED_TURN_OFF_MAX_DEVIATION_MINUTES)
711  diff = previous - new
712  if lower < diff < upper:
713  return previous
714 
715  return new
716 
717  return None
718 
719 
721  """Representation of a Xiaomi Philips Eyecare Lamp Ambient Light."""
722 
723  def __init__(self, name, device, entry, unique_id):
724  """Initialize the light device."""
725  name = f"{name} Ambient Light"
726  if unique_id is not None:
727  unique_id = f"{unique_id}-ambient"
728  super().__init__(name, device, entry, unique_id)
729 
730  async def async_turn_on(self, **kwargs: Any) -> None:
731  """Turn the light on."""
732  if ATTR_BRIGHTNESS in kwargs:
733  brightness = kwargs[ATTR_BRIGHTNESS]
734  percent_brightness = ceil(100 * brightness / 255.0)
735 
736  _LOGGER.debug(
737  "Setting brightness of the ambient light: %s %s%%",
738  brightness,
739  percent_brightness,
740  )
741 
742  result = await self._try_command_try_command(
743  "Setting brightness of the ambient failed: %s",
744  self._device_device.set_ambient_brightness,
745  percent_brightness,
746  )
747 
748  if result:
749  self._brightness_brightness_brightness = brightness
750  else:
751  await self._try_command_try_command(
752  "Turning the ambient light on failed.", self._device_device.ambient_on
753  )
754 
755  async def async_turn_off(self, **kwargs: Any) -> None:
756  """Turn the light off."""
757  await self._try_command_try_command(
758  "Turning the ambient light off failed.", self._device_device.ambient_off
759  )
760 
761  async def async_update(self) -> None:
762  """Fetch state from the device."""
763  try:
764  state = await self.hasshass.async_add_executor_job(self._device_device.status)
765  except DeviceException as ex:
766  if self._available_available_available_available:
767  self._available_available_available_available = False
768  _LOGGER.error("Got exception while fetching the state: %s", ex)
769 
770  return
771 
772  _LOGGER.debug("Got new state: %s", state)
773  self._available_available_available_available = True
774  self._state_state_state = state.ambient
775  self._brightness_brightness_brightness = ceil((255 / 100.0) * state.ambient_brightness)
776 
777 
779  """Representation of a Xiaomi Philips Zhirui Bedside Lamp."""
780 
781  _attr_supported_color_modes = {ColorMode.COLOR_TEMP, ColorMode.HS}
782 
783  def __init__(self, name, device, entry, unique_id):
784  """Initialize the light device."""
785  super().__init__(name, device, entry, unique_id)
786 
787  self._hs_color_hs_color = None
788  self._state_attrs_state_attrs.pop(ATTR_DELAYED_TURN_OFF)
789  self._state_attrs_state_attrs.update(
790  {
791  ATTR_SLEEP_ASSISTANT: None,
792  ATTR_SLEEP_OFF_TIME: None,
793  ATTR_TOTAL_ASSISTANT_SLEEP_TIME: None,
794  ATTR_BAND_SLEEP: None,
795  ATTR_BAND: None,
796  }
797  )
798 
799  @property
800  def min_mireds(self):
801  """Return the coldest color_temp that this light supports."""
802  return 153
803 
804  @property
805  def max_mireds(self):
806  """Return the warmest color_temp that this light supports."""
807  return 588
808 
809  @property
810  def hs_color(self) -> tuple[float, float] | None:
811  """Return the hs color value."""
812  return self._hs_color_hs_color
813 
814  @property
815  def color_mode(self):
816  """Return the color mode of the light."""
817  if self.hs_colorhs_colorhs_color:
818  return ColorMode.HS
819  return ColorMode.COLOR_TEMP
820 
821  async def async_turn_on(self, **kwargs: Any) -> None:
822  """Turn the light on."""
823  if ATTR_COLOR_TEMP in kwargs:
824  color_temp = kwargs[ATTR_COLOR_TEMP]
825  percent_color_temp = self.translatetranslate(
826  color_temp, self.max_miredsmax_miredsmax_miredsmax_mireds, self.min_miredsmin_miredsmin_miredsmin_mireds, CCT_MIN, CCT_MAX
827  )
828 
829  if ATTR_BRIGHTNESS in kwargs:
830  brightness = kwargs[ATTR_BRIGHTNESS]
831  percent_brightness = ceil(100 * brightness / 255.0)
832 
833  if ATTR_HS_COLOR in kwargs:
834  hs_color = kwargs[ATTR_HS_COLOR]
835  rgb = color.color_hs_to_RGB(*hs_color)
836 
837  if ATTR_BRIGHTNESS in kwargs and ATTR_HS_COLOR in kwargs:
838  _LOGGER.debug(
839  "Setting brightness and color: %s %s%%, %s",
840  brightness,
841  percent_brightness,
842  rgb,
843  )
844 
845  result = await self._try_command_try_command(
846  "Setting brightness and color failed: %s bri, %s color",
847  self._device_device.set_brightness_and_rgb,
848  percent_brightness,
849  rgb,
850  )
851 
852  if result:
853  self._hs_color_hs_color = hs_color
854  self._brightness_brightness_brightness_brightness_brightness = brightness
855 
856  elif ATTR_BRIGHTNESS in kwargs and ATTR_COLOR_TEMP in kwargs:
857  _LOGGER.debug(
858  (
859  "Setting brightness and color temperature: "
860  "%s %s%%, %s mireds, %s%% cct"
861  ),
862  brightness,
863  percent_brightness,
864  color_temp,
865  percent_color_temp,
866  )
867 
868  result = await self._try_command_try_command(
869  "Setting brightness and color temperature failed: %s bri, %s cct",
870  self._device_device.set_brightness_and_color_temperature,
871  percent_brightness,
872  percent_color_temp,
873  )
874 
875  if result:
876  self._color_temp_color_temp_color_temp = color_temp
877  self._brightness_brightness_brightness_brightness_brightness = brightness
878 
879  elif ATTR_HS_COLOR in kwargs:
880  _LOGGER.debug("Setting color: %s", rgb)
881 
882  result = await self._try_command_try_command(
883  "Setting color failed: %s", self._device_device.set_rgb, rgb
884  )
885 
886  if result:
887  self._hs_color_hs_color = hs_color
888 
889  elif ATTR_COLOR_TEMP in kwargs:
890  _LOGGER.debug(
891  "Setting color temperature: %s mireds, %s%% cct",
892  color_temp,
893  percent_color_temp,
894  )
895 
896  result = await self._try_command_try_command(
897  "Setting color temperature failed: %s cct",
898  self._device_device.set_color_temperature,
899  percent_color_temp,
900  )
901 
902  if result:
903  self._color_temp_color_temp_color_temp = color_temp
904 
905  elif ATTR_BRIGHTNESS in kwargs:
906  brightness = kwargs[ATTR_BRIGHTNESS]
907  percent_brightness = ceil(100 * brightness / 255.0)
908 
909  _LOGGER.debug("Setting brightness: %s %s%%", brightness, percent_brightness)
910 
911  result = await self._try_command_try_command(
912  "Setting brightness failed: %s",
913  self._device_device.set_brightness,
914  percent_brightness,
915  )
916 
917  if result:
918  self._brightness_brightness_brightness_brightness_brightness = brightness
919 
920  else:
921  await self._try_command_try_command("Turning the light on failed.", self._device_device.on)
922 
923  async def async_update(self) -> None:
924  """Fetch state from the device."""
925  try:
926  state = await self.hasshass.async_add_executor_job(self._device_device.status)
927  except DeviceException as ex:
930  _LOGGER.error("Got exception while fetching the state: %s", ex)
931 
932  return
933 
934  _LOGGER.debug("Got new state: %s", state)
936  self._state_state_state_state_state = state.is_on
937  self._brightness_brightness_brightness_brightness_brightness = ceil((255 / 100.0) * state.brightness)
938  self._color_temp_color_temp_color_temp = self.translatetranslate(
939  state.color_temperature, CCT_MIN, CCT_MAX, self.max_miredsmax_miredsmax_miredsmax_mireds, self.min_miredsmin_miredsmin_miredsmin_mireds
940  )
941  self._hs_color_hs_color = color.color_RGB_to_hs(*state.rgb)
942 
943  self._state_attrs_state_attrs.update(
944  {
945  ATTR_SCENE: state.scene,
946  ATTR_SLEEP_ASSISTANT: state.sleep_assistant,
947  ATTR_SLEEP_OFF_TIME: state.sleep_off_time,
948  ATTR_TOTAL_ASSISTANT_SLEEP_TIME: state.total_assistant_sleep_time,
949  ATTR_BAND_SLEEP: state.brand_sleep,
950  ATTR_BAND: state.brand,
951  }
952  )
953 
954  async def async_set_delayed_turn_off(self, time_period: timedelta):
955  """Set delayed turn off. Unsupported."""
956  return
957 
958 
960  """Representation of a gateway device's light."""
961 
962  _attr_color_mode = ColorMode.HS
963  _attr_supported_color_modes = {ColorMode.HS}
964 
965  def __init__(self, gateway_device, gateway_name, gateway_device_id):
966  """Initialize the XiaomiGatewayLight."""
967  self._gateway_gateway = gateway_device
968  self._name_name = f"{gateway_name} Light"
969  self._gateway_device_id_gateway_device_id = gateway_device_id
970  self._unique_id_unique_id = gateway_device_id
971  self._available_available = False
972  self._is_on_is_on = None
973  self._brightness_pct_brightness_pct = 100
974  self._rgb_rgb = (255, 255, 255)
975  self._hs_hs = (0, 0)
976 
977  @property
978  def unique_id(self):
979  """Return an unique ID."""
980  return self._unique_id_unique_id
981 
982  @property
983  def device_info(self) -> DeviceInfo:
984  """Return the device info of the gateway."""
985  return DeviceInfo(
986  identifiers={(DOMAIN, self._gateway_device_id_gateway_device_id)},
987  )
988 
989  @property
990  def name(self):
991  """Return the name of this entity, if any."""
992  return self._name_name
993 
994  @property
995  def available(self):
996  """Return true when state is known."""
997  return self._available_available
998 
999  @property
1000  def is_on(self):
1001  """Return true if it is on."""
1002  return self._is_on_is_on
1003 
1004  @property
1005  def brightness(self):
1006  """Return the brightness of this light between 0..255."""
1007  return int(255 * self._brightness_pct_brightness_pct / 100)
1008 
1009  @property
1010  def hs_color(self):
1011  """Return the hs color value."""
1012  return self._hs_hs
1013 
1014  def turn_on(self, **kwargs: Any) -> None:
1015  """Turn the light on."""
1016  if ATTR_HS_COLOR in kwargs:
1017  rgb = color.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR])
1018  else:
1019  rgb = self._rgb_rgb
1020 
1021  if ATTR_BRIGHTNESS in kwargs:
1022  brightness_pct = int(100 * kwargs[ATTR_BRIGHTNESS] / 255)
1023  else:
1024  brightness_pct = self._brightness_pct_brightness_pct
1025 
1026  self._gateway_gateway.light.set_rgb(brightness_pct, rgb)
1027 
1028  self.schedule_update_ha_stateschedule_update_ha_state()
1029 
1030  def turn_off(self, **kwargs: Any) -> None:
1031  """Turn the light off."""
1032  self._gateway_gateway.light.set_rgb(0, self._rgb_rgb)
1033  self.schedule_update_ha_stateschedule_update_ha_state()
1034 
1035  async def async_update(self) -> None:
1036  """Fetch state from the device."""
1037  try:
1038  state_dict = await self.hasshass.async_add_executor_job(
1039  self._gateway_gateway.light.rgb_status
1040  )
1041  except GatewayException as ex:
1042  if self._available_available:
1043  self._available_available = False
1044  _LOGGER.error(
1045  "Got exception while fetching the gateway light state: %s", ex
1046  )
1047  return
1048 
1049  self._available_available = True
1050  self._is_on_is_on = state_dict["is_on"]
1051 
1052  if self._is_on_is_on:
1053  self._brightness_pct_brightness_pct = state_dict["brightness"]
1054  self._rgb_rgb = state_dict["rgb"]
1055  self._hs_hs = color.color_RGB_to_hs(*self._rgb_rgb)
1056 
1057 
1059  """Representation of Xiaomi Gateway Bulb."""
1060 
1061  _attr_color_mode = ColorMode.COLOR_TEMP
1062  _attr_supported_color_modes = {ColorMode.COLOR_TEMP}
1063 
1064  @property
1065  def brightness(self):
1066  """Return the brightness of the light."""
1067  return round((self._sub_device_sub_device.status["brightness"] * 255) / 100)
1068 
1069  @property
1070  def color_temp(self):
1071  """Return current color temperature."""
1072  return self._sub_device_sub_device.status["color_temp"]
1073 
1074  @property
1075  def is_on(self):
1076  """Return true if light is on."""
1077  return self._sub_device_sub_device.status["status"] == "on"
1078 
1079  @property
1080  def min_mireds(self):
1081  """Return min cct."""
1082  return self._sub_device_sub_device.status["cct_min"]
1083 
1084  @property
1085  def max_mireds(self):
1086  """Return max cct."""
1087  return self._sub_device_sub_device.status["cct_max"]
1088 
1089  async def async_turn_on(self, **kwargs: Any) -> None:
1090  """Instruct the light to turn on."""
1091  await self.hasshass.async_add_executor_job(self._sub_device_sub_device.on)
1092 
1093  if ATTR_COLOR_TEMP in kwargs:
1094  color_temp = kwargs[ATTR_COLOR_TEMP]
1095  await self.hasshass.async_add_executor_job(
1096  self._sub_device_sub_device.set_color_temp, color_temp
1097  )
1098 
1099  if ATTR_BRIGHTNESS in kwargs:
1100  brightness = round((kwargs[ATTR_BRIGHTNESS] * 100) / 255)
1101  await self.hasshass.async_add_executor_job(
1102  self._sub_device_sub_device.set_brightness, brightness
1103  )
1104 
1105  async def async_turn_off(self, **kwargs: Any) -> None:
1106  """Instruct the light to turn off."""
1107  await self.hasshass.async_add_executor_job(self._sub_device_sub_device.off)
tuple[float, float]|None hs_color(self)
Definition: __init__.py:947
def __init__(self, gateway_device, gateway_name, gateway_device_id)
Definition: light.py:965
def _try_command(self, mask_error, func, *args, **kwargs)
Definition: light.py:293
def __init__(self, name, device, entry, unique_id)
Definition: light.py:264
def translate(value, left_min, left_max, right_min, right_max)
Definition: light.py:543
def __init__(self, name, device, entry, unique_id)
Definition: light.py:426
def __init__(self, name, device, entry, unique_id)
Definition: light.py:554
def async_set_delayed_turn_off(self, timedelta time_period)
Definition: light.py:650
def __init__(self, name, device, entry, unique_id)
Definition: light.py:610
def delayed_turn_off_timestamp(int countdown, datetime.datetime current, datetime.datetime previous)
Definition: light.py:699
def __init__(self, name, device, entry, unique_id)
Definition: light.py:352
def delayed_turn_off_timestamp(int countdown, datetime.datetime current, datetime.datetime previous)
Definition: light.py:401
def __init__(self, name, device, entry, unique_id)
Definition: light.py:783
None schedule_update_ha_state(self, bool force_refresh=False)
Definition: entity.py:1244
IssData update(pyiss.ISS iss)
Definition: __init__.py:33
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: light.py:136