Home Assistant Unofficial Reference 2024.12.1
device.py
Go to the documentation of this file.
1 """Support for Xiaomi Yeelight WiFi color bulb."""
2 
3 from __future__ import annotations
4 
5 import logging
6 from typing import Any
7 
8 from yeelight import BulbException
9 from yeelight.aio import KEY_CONNECTED, AsyncBulb
10 
11 from homeassistant.const import CONF_ID, CONF_NAME
12 from homeassistant.core import HomeAssistant, callback
13 from homeassistant.helpers.dispatcher import async_dispatcher_send
14 from homeassistant.helpers.event import async_call_later
15 
16 from .const import (
17  ACTIVE_COLOR_FLOWING,
18  ACTIVE_MODE_NIGHTLIGHT,
19  DATA_UPDATED,
20  STATE_CHANGE_TIME,
21  UPDATE_REQUEST_PROPERTIES,
22 )
23 from .scanner import YeelightScanner
24 
25 _LOGGER = logging.getLogger(__name__)
26 
27 
28 @callback
29 def async_format_model(model: str) -> str:
30  """Generate a more human readable model."""
31  return model.replace("_", " ").title()
32 
33 
34 @callback
35 def async_format_id(id_: str | None) -> str:
36  """Generate a more human readable id."""
37  return hex(int(id_, 16)) if id_ else "None"
38 
39 
40 @callback
41 def async_format_model_id(model: str, id_: str | None) -> str:
42  """Generate a more human readable name."""
43  return f"{async_format_model(model)} {async_format_id(id_)}"
44 
45 
46 @callback
47 def _async_unique_name(capabilities: dict) -> str:
48  """Generate name from capabilities."""
49  model_id = async_format_model_id(capabilities["model"], capabilities["id"])
50  return f"Yeelight {model_id}"
51 
52 
54  """Check if a push update needs the bg_power workaround.
55 
56  Some devices will push the incorrect state for bg_power.
57 
58  To work around this any time we are pushed an update
59  with bg_power, we force poll state which will be correct.
60  """
61  return "bg_power" in data
62 
63 
65  """Represents single Yeelight device."""
66 
67  def __init__(
68  self, hass: HomeAssistant, host: str, config: dict[str, Any], bulb: AsyncBulb
69  ) -> None:
70  """Initialize device."""
71  self._hass_hass = hass
72  self._config_config = config
73  self._host_host = host
74  self._bulb_device_bulb_device = bulb
75  self.capabilitiescapabilities: dict[str, Any] = {}
76  self._device_type_device_type: str | None = None
77  self._available_available = True
78  self._initialized_initialized = False
79  self._name_name: str | None = None
80 
81  @property
82  def bulb(self):
83  """Return bulb device."""
84  return self._bulb_device_bulb_device
85 
86  @property
87  def name(self):
88  """Return the name of the device if any."""
89  return self._name_name
90 
91  @property
92  def config(self):
93  """Return device config."""
94  return self._config_config
95 
96  @property
97  def host(self):
98  """Return hostname."""
99  return self._host_host
100 
101  @property
102  def available(self):
103  """Return true is device is available."""
104  return self._available_available
105 
106  @callback
108  """Set unavailable on api call failure due to a network issue."""
109  self._available_available = False
110 
111  @property
112  def model(self):
113  """Return configured/autodetected device model."""
114  return self._bulb_device_bulb_device.model or self.capabilitiescapabilities.get("model")
115 
116  @property
117  def fw_version(self):
118  """Return the firmware version."""
119  return self.capabilitiescapabilities.get("fw_ver")
120 
121  @property
122  def unique_id(self) -> str | None:
123  """Return the unique ID of the device."""
124  return self.capabilitiescapabilities.get("id")
125 
126  @property
127  def is_nightlight_supported(self) -> bool:
128  """Return true / false if nightlight is supported.
129 
130  Uses brightness as it appears to be supported in both ceiling and other lights.
131  """
132  return self._nightlight_brightness_nightlight_brightness is not None
133 
134  @property
135  def is_nightlight_enabled(self) -> bool:
136  """Return true / false if nightlight is currently enabled."""
137  # Only ceiling lights have active_mode, from SDK docs:
138  # active_mode 0: daylight mode / 1: moonlight mode (ceiling light only)
139  if self._active_mode_active_mode is not None:
140  return int(self._active_mode_active_mode) == ACTIVE_MODE_NIGHTLIGHT
141 
142  if self._nightlight_brightness_nightlight_brightness is not None:
143  return int(self._nightlight_brightness_nightlight_brightness) > 0
144 
145  return False
146 
147  @property
148  def is_color_flow_enabled(self) -> bool:
149  """Return true / false if color flow is currently running."""
150  return self._color_flow_color_flow and int(self._color_flow_color_flow) == ACTIVE_COLOR_FLOWING
151 
152  @property
153  def _active_mode(self):
154  return self.bulbbulb.last_properties.get("active_mode")
155 
156  @property
157  def _color_flow(self):
158  return self.bulbbulb.last_properties.get("flowing")
159 
160  @property
162  return self.bulbbulb.last_properties.get("nl_br")
163 
164  @property
165  def type(self):
166  """Return bulb type."""
167  if not self._device_type_device_type:
168  self._device_type_device_type = self.bulbbulb.bulb_type
169 
170  return self._device_type_device_type
171 
172  async def _async_update_properties(self):
173  """Read new properties from the device."""
174  try:
175  await self.bulbbulb.async_get_properties(UPDATE_REQUEST_PROPERTIES)
176  self._available_available = True
177  if not self._initialized_initialized:
178  self._initialized_initialized = True
179  except TimeoutError as ex:
180  _LOGGER.debug(
181  "timed out while trying to update device %s, %s: %s",
182  self._host_host,
183  self.namename,
184  ex,
185  )
186  except OSError as ex:
187  if self._available_available: # just inform once
188  _LOGGER.error(
189  "Unable to update device %s, %s: %s", self._host_host, self.namename, ex
190  )
191  self._available_available = False
192  except BulbException as ex:
193  _LOGGER.debug(
194  "Unable to update device %s, %s: %s", self._host_host, self.namename, ex
195  )
196 
197  async def async_setup(self):
198  """Fetch capabilities and setup name if available."""
199  scanner = YeelightScanner.async_get(self._hass_hass)
200  self.capabilitiescapabilities = await scanner.async_get_capabilities(self._host_host) or {}
201  if self.capabilitiescapabilities:
202  self._bulb_device_bulb_device.set_capabilities(self.capabilitiescapabilities)
203  if name := self._config_config.get(CONF_NAME):
204  # Override default name when name is set in config
205  self._name_name = name
206  elif self.capabilitiescapabilities:
207  # Generate name from model and id when capabilities is available
208  self._name_name = _async_unique_name(self.capabilitiescapabilities)
209  elif self.modelmodel and (id_ := self._config_config.get(CONF_ID)):
210  self._name_name = f"Yeelight {async_format_model_id(self.model, id_)}"
211  else:
212  self._name_name = self._host_host # Default name is host
213 
214  async def async_update(self, force=False):
215  """Update device properties and send data updated signal."""
216  if not force and self._initialized_initialized and self._available_available:
217  # No need to poll unless force, already connected
218  return
219  await self._async_update_properties_async_update_properties()
220  async_dispatcher_send(self._hass_hass, DATA_UPDATED.format(self._host_host))
221 
222  async def _async_forced_update(self, _now):
223  """Call a forced update."""
224  await self.async_updateasync_update(True)
225 
226  @callback
227  def async_update_callback(self, data):
228  """Update push from device."""
229  _LOGGER.debug("Received callback: %s", data)
230  was_available = self._available_available
231  self._available_available = data.get(KEY_CONNECTED, True)
233  not was_available and self._available_available
234  ):
235  # On reconnect the properties may be out of sync
236  #
237  # If the device drops the connection right away, we do not want to
238  # do a property resync via async_update since its about
239  # to be called when async_setup_entry reaches the end of the
240  # function
241  #
242  async_call_later(self._hass_hass, STATE_CHANGE_TIME, self._async_forced_update_async_forced_update)
243  async_dispatcher_send(self._hass_hass, DATA_UPDATED.format(self._host_host))
None __init__(self, HomeAssistant hass, str host, dict[str, Any] config, AsyncBulb bulb)
Definition: device.py:69
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
str _async_unique_name(dict capabilities)
Definition: device.py:47
str async_format_model_id(str model, str|None id_)
Definition: device.py:41
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)
Definition: dispatcher.py:193
CALLBACK_TYPE async_call_later(HomeAssistant hass, float|timedelta delay, HassJob[[datetime], Coroutine[Any, Any, None]|None]|Callable[[datetime], Coroutine[Any, Any, None]|None] action)
Definition: event.py:1597