Home Assistant Unofficial Reference 2024.12.1
__init__.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 
7 import voluptuous as vol
8 from yeelight import BulbException
9 from yeelight.aio import AsyncBulb
10 
11 from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
12 from homeassistant.const import (
13  CONF_DEVICES,
14  CONF_HOST,
15  CONF_ID,
16  CONF_MODEL,
17  CONF_NAME,
18  EVENT_HOMEASSISTANT_STOP,
19 )
20 from homeassistant.core import HomeAssistant, callback
21 from homeassistant.exceptions import ConfigEntryNotReady
23 from homeassistant.helpers.typing import ConfigType, VolDictType
24 
25 from .const import (
26  ACTION_OFF,
27  ACTION_RECOVER,
28  ACTION_STAY,
29  ATTR_ACTION,
30  ATTR_COUNT,
31  ATTR_TRANSITIONS,
32  CONF_CUSTOM_EFFECTS,
33  CONF_DETECTED_MODEL,
34  CONF_FLOW_PARAMS,
35  CONF_MODE_MUSIC,
36  CONF_NIGHTLIGHT_SWITCH,
37  CONF_NIGHTLIGHT_SWITCH_TYPE,
38  CONF_SAVE_ON_CHANGE,
39  CONF_TRANSITION,
40  DATA_CONFIG_ENTRIES,
41  DATA_CUSTOM_EFFECTS,
42  DATA_DEVICE,
43  DEFAULT_MODE_MUSIC,
44  DEFAULT_NAME,
45  DEFAULT_NIGHTLIGHT_SWITCH,
46  DEFAULT_SAVE_ON_CHANGE,
47  DEFAULT_TRANSITION,
48  DOMAIN,
49  NIGHTLIGHT_SWITCH_TYPE_LIGHT,
50  PLATFORMS,
51  YEELIGHT_HSV_TRANSACTION,
52  YEELIGHT_RGB_TRANSITION,
53  YEELIGHT_SLEEP_TRANSACTION,
54  YEELIGHT_TEMPERATURE_TRANSACTION,
55 )
56 from .device import YeelightDevice, async_format_id
57 from .scanner import YeelightScanner
58 
59 _LOGGER = logging.getLogger(__name__)
60 
61 
62 YEELIGHT_FLOW_TRANSITION_SCHEMA: VolDictType = {
63  vol.Optional(ATTR_COUNT, default=0): cv.positive_int,
64  vol.Optional(ATTR_ACTION, default=ACTION_RECOVER): vol.Any(
65  ACTION_RECOVER, ACTION_OFF, ACTION_STAY
66  ),
67  vol.Required(ATTR_TRANSITIONS): [
68  {
69  vol.Exclusive(YEELIGHT_RGB_TRANSITION, CONF_TRANSITION): vol.All(
70  cv.ensure_list, [cv.positive_int]
71  ),
72  vol.Exclusive(YEELIGHT_HSV_TRANSACTION, CONF_TRANSITION): vol.All(
73  cv.ensure_list, [cv.positive_int]
74  ),
75  vol.Exclusive(YEELIGHT_TEMPERATURE_TRANSACTION, CONF_TRANSITION): vol.All(
76  cv.ensure_list, [cv.positive_int]
77  ),
78  vol.Exclusive(YEELIGHT_SLEEP_TRANSACTION, CONF_TRANSITION): vol.All(
79  cv.ensure_list, [cv.positive_int]
80  ),
81  }
82  ],
83 }
84 
85 DEVICE_SCHEMA = vol.Schema(
86  {
87  vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
88  vol.Optional(CONF_TRANSITION, default=DEFAULT_TRANSITION): cv.positive_int,
89  vol.Optional(CONF_MODE_MUSIC, default=False): cv.boolean,
90  vol.Optional(CONF_SAVE_ON_CHANGE, default=False): cv.boolean,
91  vol.Optional(CONF_NIGHTLIGHT_SWITCH_TYPE): vol.Any(
92  NIGHTLIGHT_SWITCH_TYPE_LIGHT
93  ),
94  vol.Optional(CONF_MODEL): cv.string,
95  }
96 )
97 
98 CONFIG_SCHEMA = vol.Schema(
99  {
100  DOMAIN: vol.Schema(
101  {
102  vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA},
103  vol.Optional(CONF_CUSTOM_EFFECTS): [
104  {
105  vol.Required(CONF_NAME): cv.string,
106  vol.Required(CONF_FLOW_PARAMS): YEELIGHT_FLOW_TRANSITION_SCHEMA,
107  }
108  ],
109  }
110  )
111  },
112  extra=vol.ALLOW_EXTRA,
113 )
114 
115 
116 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
117  """Set up the Yeelight bulbs."""
118  conf = config.get(DOMAIN, {})
119  hass.data[DOMAIN] = {
120  DATA_CUSTOM_EFFECTS: conf.get(CONF_CUSTOM_EFFECTS, {}),
121  DATA_CONFIG_ENTRIES: {},
122  }
123  # Make sure the scanner is always started in case we are
124  # going to retry via ConfigEntryNotReady and the bulb has changed
125  # ip
126  scanner = YeelightScanner.async_get(hass)
127  await scanner.async_setup()
128 
129  # Import manually configured devices
130  for host, device_config in config.get(DOMAIN, {}).get(CONF_DEVICES, {}).items():
131  _LOGGER.debug("Importing configured %s", host)
132  entry_config = {CONF_HOST: host, **device_config}
133  hass.async_create_task(
134  hass.config_entries.flow.async_init(
135  DOMAIN, context={"source": SOURCE_IMPORT}, data=entry_config
136  )
137  )
138 
139  return True
140 
141 
143  hass: HomeAssistant,
144  entry: ConfigEntry,
145  device: YeelightDevice,
146 ) -> None:
147  """Initialize a Yeelight device."""
148  entry_data = hass.data[DOMAIN][DATA_CONFIG_ENTRIES][entry.entry_id] = {}
149  await device.async_setup()
150  entry_data[DATA_DEVICE] = device
151 
152  if (
153  device.capabilities
154  and entry.data.get(CONF_DETECTED_MODEL) != device.capabilities["model"]
155  ):
156  hass.config_entries.async_update_entry(
157  entry,
158  data={**entry.data, CONF_DETECTED_MODEL: device.capabilities["model"]},
159  )
160 
161 
162 @callback
163 def _async_normalize_config_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
164  """Move options from data for imported entries.
165 
166  Initialize options with default values for other entries.
167 
168  Copy the unique id to CONF_ID if it is missing
169  """
170  if not entry.options:
171  hass.config_entries.async_update_entry(
172  entry,
173  data={
174  CONF_HOST: entry.data.get(CONF_HOST),
175  CONF_ID: entry.data.get(CONF_ID) or entry.unique_id,
176  CONF_DETECTED_MODEL: entry.data.get(CONF_DETECTED_MODEL),
177  },
178  options={
179  CONF_NAME: entry.data.get(CONF_NAME, ""),
180  CONF_MODEL: entry.data.get(
181  CONF_MODEL, entry.data.get(CONF_DETECTED_MODEL, "")
182  ),
183  CONF_TRANSITION: entry.data.get(CONF_TRANSITION, DEFAULT_TRANSITION),
184  CONF_MODE_MUSIC: entry.data.get(CONF_MODE_MUSIC, DEFAULT_MODE_MUSIC),
185  CONF_SAVE_ON_CHANGE: entry.data.get(
186  CONF_SAVE_ON_CHANGE, DEFAULT_SAVE_ON_CHANGE
187  ),
188  CONF_NIGHTLIGHT_SWITCH: entry.data.get(
189  CONF_NIGHTLIGHT_SWITCH, DEFAULT_NIGHTLIGHT_SWITCH
190  ),
191  },
192  unique_id=entry.unique_id or entry.data.get(CONF_ID),
193  )
194  elif entry.unique_id and not entry.data.get(CONF_ID):
195  hass.config_entries.async_update_entry(
196  entry,
197  data={CONF_HOST: entry.data.get(CONF_HOST), CONF_ID: entry.unique_id},
198  )
199  elif entry.data.get(CONF_ID) and not entry.unique_id:
200  hass.config_entries.async_update_entry(
201  entry,
202  unique_id=entry.data[CONF_ID],
203  )
204 
205 
206 async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
207  """Set up Yeelight from a config entry."""
208  _async_normalize_config_entry(hass, entry)
209 
210  if not entry.data.get(CONF_HOST):
211  bulb_id = async_format_id(entry.data.get(CONF_ID, entry.unique_id))
212  raise ConfigEntryNotReady(f"Waiting for {bulb_id} to be discovered")
213 
214  try:
215  device = await _async_get_device(hass, entry.data[CONF_HOST], entry)
216  await _async_initialize(hass, entry, device)
217  except (TimeoutError, OSError, BulbException) as ex:
218  raise ConfigEntryNotReady from ex
219 
220  found_unique_id = device.unique_id
221  expected_unique_id = entry.unique_id
222  if expected_unique_id and found_unique_id and found_unique_id != expected_unique_id:
223  # If the id of the device does not match the unique_id
224  # of the config entry, it likely means the DHCP lease has expired
225  # and the device has been assigned a new IP address. We need to
226  # wait for the next discovery to find the device at its new address
227  # and update the config entry so we do not mix up devices.
228  raise ConfigEntryNotReady(
229  f"Unexpected device found at {device.host}; "
230  f"expected {expected_unique_id}, found {found_unique_id}"
231  )
232 
233  await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
234 
235  # Wait to install the reload listener until everything was successfully initialized
236  entry.async_on_unload(entry.add_update_listener(_async_update_listener))
237 
238  return True
239 
240 
241 async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
242  """Unload a config entry."""
243  data_config_entries = hass.data[DOMAIN][DATA_CONFIG_ENTRIES]
244  data_config_entries.pop(entry.entry_id)
245  return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
246 
247 
248 async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
249  """Handle options update."""
250  await hass.config_entries.async_reload(entry.entry_id)
251 
252 
254  hass: HomeAssistant, host: str, entry: ConfigEntry
255 ) -> YeelightDevice:
256  # Get model from config and capabilities
257  model = entry.options.get(CONF_MODEL) or entry.data.get(CONF_DETECTED_MODEL)
258 
259  # Set up device
260  bulb = AsyncBulb(host, model=model or None)
261 
262  device = YeelightDevice(hass, host, {**entry.options, **entry.data}, bulb)
263  # start listening for local pushes
264  await device.bulb.async_listen(device.async_update_callback)
265 
266  # register stop callback to shutdown listening for local pushes
267  async def async_stop_listen_task(event):
268  """Stop listen task."""
269  _LOGGER.debug("Shutting down Yeelight Listener (stop event)")
270  await device.bulb.async_stop_listening()
271 
272  @callback
273  def _async_stop_listen_on_unload():
274  """Stop listen task."""
275  _LOGGER.debug("Shutting down Yeelight Listener (unload)")
276  hass.async_create_task(device.bulb.async_stop_listening())
277 
278  entry.async_on_unload(
279  hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_listen_task)
280  )
281  entry.async_on_unload(_async_stop_listen_on_unload)
282 
283  # fetch initial state
284  await device.async_update()
285 
286  if (
287  # Must have last_properties
288  not device.bulb.last_properties
289  # Must have at least a power property
290  or (
291  "main_power" not in device.bulb.last_properties
292  and "power" not in device.bulb.last_properties
293  )
294  ):
295  raise ConfigEntryNotReady(
296  "Could not fetch initial state; try power cycling the device"
297  )
298 
299  return device
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:116
None _async_update_listener(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:248
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:206
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:241
None _async_normalize_config_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:163
None _async_initialize(HomeAssistant hass, ConfigEntry entry, YeelightDevice device)
Definition: __init__.py:146
YeelightDevice _async_get_device(HomeAssistant hass, str host, ConfigEntry entry)
Definition: __init__.py:255