Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Support for LIFX."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from collections.abc import Iterable
7 from datetime import datetime, timedelta
8 import socket
9 from typing import Any
10 
11 from aiolifx.aiolifx import Light
12 from aiolifx.connection import LIFXConnection
13 import voluptuous as vol
14 
15 from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
16 from homeassistant.config_entries import ConfigEntry
17 from homeassistant.const import (
18  CONF_HOST,
19  CONF_PORT,
20  EVENT_HOMEASSISTANT_STARTED,
21  Platform,
22 )
23 from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback
24 from homeassistant.exceptions import ConfigEntryNotReady
26 from homeassistant.helpers.event import async_call_later, async_track_time_interval
27 from homeassistant.helpers.typing import ConfigType
28 
29 from .const import _LOGGER, DATA_LIFX_MANAGER, DOMAIN, TARGET_ANY
30 from .coordinator import LIFXUpdateCoordinator
31 from .discovery import async_discover_devices, async_trigger_discovery
32 from .manager import LIFXManager
33 from .migration import async_migrate_entities_devices, async_migrate_legacy_entries
34 from .util import async_entry_is_legacy, async_get_legacy_entry, formatted_serial
35 
36 CONF_SERVER = "server"
37 CONF_BROADCAST = "broadcast"
38 
39 
40 INTERFACE_SCHEMA = vol.Schema(
41  {
42  vol.Optional(CONF_SERVER): cv.string,
43  vol.Optional(CONF_PORT): cv.port,
44  vol.Optional(CONF_BROADCAST): cv.string,
45  }
46 )
47 
48 CONFIG_SCHEMA = vol.All(
49  cv.deprecated(DOMAIN),
50  vol.Schema(
51  {
52  DOMAIN: {
53  LIGHT_DOMAIN: vol.Schema(vol.All(cv.ensure_list, [INTERFACE_SCHEMA]))
54  }
55  },
56  extra=vol.ALLOW_EXTRA,
57  ),
58 )
59 
60 
61 PLATFORMS = [
62  Platform.BINARY_SENSOR,
63  Platform.BUTTON,
64  Platform.LIGHT,
65  Platform.SELECT,
66  Platform.SENSOR,
67 ]
68 DISCOVERY_INTERVAL = timedelta(minutes=15)
69 MIGRATION_INTERVAL = timedelta(minutes=5)
70 
71 DISCOVERY_COOLDOWN = 5
72 
73 
75  hass: HomeAssistant,
76  legacy_entry: ConfigEntry,
77  discovered_devices: Iterable[Light],
78 ) -> bool:
79  """Migrate config entries."""
80  existing_serials = {
81  entry.unique_id
82  for entry in hass.config_entries.async_entries(DOMAIN)
83  if entry.unique_id and not async_entry_is_legacy(entry)
84  }
85  # device.mac_addr is not the mac_address, its the serial number
86  hosts_by_serial = {device.mac_addr: device.ip_addr for device in discovered_devices}
87  missing_discovery_count = async_migrate_legacy_entries(
88  hass, hosts_by_serial, existing_serials, legacy_entry
89  )
90  if missing_discovery_count:
91  _LOGGER.debug(
92  "Migration in progress, waiting to discover %s device(s)",
93  missing_discovery_count,
94  )
95  return False
96 
97  _LOGGER.debug(
98  "Migration successful, removing legacy entry %s", legacy_entry.entry_id
99  )
100  await hass.config_entries.async_remove(legacy_entry.entry_id)
101  return True
102 
103 
105  """Manage discovery and migration."""
106 
107  def __init__(self, hass: HomeAssistant, migrating: bool) -> None:
108  """Init the manager."""
109  self.hasshass = hass
110  self.locklock = asyncio.Lock()
111  self.migratingmigrating = migrating
112  self._cancel_discovery_cancel_discovery: CALLBACK_TYPE | None = None
113 
114  @callback
116  """Set up discovery at an interval."""
117  if self._cancel_discovery_cancel_discovery:
118  self._cancel_discovery_cancel_discovery()
119  self._cancel_discovery_cancel_discovery = None
120  discovery_interval = (
121  MIGRATION_INTERVAL if self.migratingmigrating else DISCOVERY_INTERVAL
122  )
123  _LOGGER.debug(
124  "LIFX starting discovery with interval: %s and migrating: %s",
125  discovery_interval,
126  self.migratingmigrating,
127  )
128  self._cancel_discovery_cancel_discovery = async_track_time_interval(
129  self.hasshass, self.async_discoveryasync_discovery, discovery_interval, cancel_on_shutdown=True
130  )
131 
132  async def async_discovery(self, *_: Any) -> None:
133  """Discovery and migrate LIFX devics."""
134  migrating_was_in_progress = self.migratingmigrating
135 
136  async with self.locklock:
137  discovered = await async_discover_devices(self.hasshass)
138 
139  if legacy_entry := async_get_legacy_entry(self.hasshass):
140  migration_complete = await async_legacy_migration(
141  self.hasshass, legacy_entry, discovered
142  )
143  if migration_complete and migrating_was_in_progress:
144  self.migratingmigrating = False
145  _LOGGER.debug(
146  (
147  "LIFX migration complete, switching to normal discovery"
148  " interval: %s"
149  ),
150  DISCOVERY_INTERVAL,
151  )
152  self.async_setup_discovery_intervalasync_setup_discovery_interval()
153 
154  if discovered:
155  async_trigger_discovery(self.hasshass, discovered)
156 
157 
158 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
159  """Set up the LIFX component."""
160  hass.data[DOMAIN] = {}
161  migrating = bool(async_get_legacy_entry(hass))
162  discovery_manager = LIFXDiscoveryManager(hass, migrating)
163 
164  @callback
165  def _async_delayed_discovery(now: datetime) -> None:
166  """Start an untracked task to discover devices.
167 
168  We do not want the discovery task to block startup.
169  """
170  hass.async_create_background_task(
171  discovery_manager.async_discovery(), "lifx-discovery"
172  )
173 
174  # Let the system settle a bit before starting discovery
175  # to reduce the risk we miss devices because the event
176  # loop is blocked at startup.
177  discovery_manager.async_setup_discovery_interval()
179  hass,
180  DISCOVERY_COOLDOWN,
181  HassJob(_async_delayed_discovery, cancel_on_shutdown=True),
182  )
183  hass.bus.async_listen_once(
184  EVENT_HOMEASSISTANT_STARTED, discovery_manager.async_discovery
185  )
186 
187  return True
188 
189 
190 async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
191  """Set up LIFX from a config entry."""
192  if async_entry_is_legacy(entry):
193  return True
194 
195  if legacy_entry := async_get_legacy_entry(hass):
196  # If the legacy entry still exists, harvest the entities
197  # that are moving to this config entry.
198  async_migrate_entities_devices(hass, legacy_entry.entry_id, entry)
199 
200  assert entry.unique_id is not None
201  domain_data = hass.data[DOMAIN]
202  if DATA_LIFX_MANAGER not in domain_data:
203  manager = LIFXManager(hass)
204  domain_data[DATA_LIFX_MANAGER] = manager
205  manager.async_setup()
206 
207  host = entry.data[CONF_HOST]
208  connection = LIFXConnection(host, TARGET_ANY)
209  try:
210  await connection.async_setup()
211  except socket.gaierror as ex:
212  connection.async_stop()
213  raise ConfigEntryNotReady(f"Could not resolve {host}: {ex}") from ex
214  coordinator = LIFXUpdateCoordinator(hass, connection, entry.title)
215  coordinator.async_setup()
216  try:
217  await coordinator.async_config_entry_first_refresh()
218  except ConfigEntryNotReady:
219  connection.async_stop()
220  raise
221 
222  serial = formatted_serial(coordinator.serial_number)
223  if serial != entry.unique_id:
224  # If the serial number of the device does not match the unique_id
225  # of the config entry, it likely means the DHCP lease has expired
226  # and the device has been assigned a new IP address. We need to
227  # wait for the next discovery to find the device at its new address
228  # and update the config entry so we do not mix up devices.
229  raise ConfigEntryNotReady(
230  f"Unexpected device found at {host}; expected {entry.unique_id}, found {serial}"
231  )
232  domain_data[entry.entry_id] = coordinator
233  await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
234  return True
235 
236 
237 async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
238  """Unload a config entry."""
239  if async_entry_is_legacy(entry):
240  return True
241  domain_data = hass.data[DOMAIN]
242  if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
243  coordinator: LIFXUpdateCoordinator = domain_data.pop(entry.entry_id)
244  coordinator.connection.async_stop()
245  # Only the DATA_LIFX_MANAGER left, remove it.
246  if len(domain_data) == 1:
247  manager: LIFXManager = domain_data.pop(DATA_LIFX_MANAGER)
248  manager.async_unload()
249  return unload_ok
None __init__(self, HomeAssistant hass, bool migrating)
Definition: __init__.py:107
None async_migrate_entities_devices(HomeAssistant hass, str legacy_entry_id, ConfigEntry new_entry)
Definition: migration.py:49
int async_migrate_legacy_entries(HomeAssistant hass, dict[str, str] discovered_hosts_by_serial, set[str] existing_serials, ConfigEntry legacy_entry)
Definition: migration.py:19
ConfigEntry|None async_get_legacy_entry(HomeAssistant hass)
Definition: util.py:49
bool async_entry_is_legacy(ConfigEntry entry)
Definition: util.py:43
str formatted_serial(str serial_number)
Definition: util.py:171
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:190
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:237
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:158
bool async_legacy_migration(HomeAssistant hass, ConfigEntry legacy_entry, Iterable[Light] discovered_devices)
Definition: __init__.py:78
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
CALLBACK_TYPE async_track_time_interval(HomeAssistant hass, Callable[[datetime], Coroutine[Any, Any, None]|None] action, timedelta interval, *str|None name=None, bool|None cancel_on_shutdown=None)
Definition: event.py:1679