|
1 | 1 | """The UniLED integration."""
|
| 2 | + |
2 | 3 | from __future__ import annotations
|
3 | 4 |
|
| 5 | +from typing import Any, Final, cast |
| 6 | + |
4 | 7 | from homeassistant.components import bluetooth
|
5 | 8 | from homeassistant.components.bluetooth.match import (
|
6 | 9 | BluetoothCallbackMatcher,
|
|
11 | 14 | from homeassistant.core import Event, HomeAssistant, callback, CoreState
|
12 | 15 | from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
|
13 | 16 | from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
| 17 | +from homeassistant.helpers.typing import ConfigType |
14 | 18 | from homeassistant.helpers.device_registry import format_mac
|
15 | 19 | from homeassistant.helpers.entity_registry import async_get, async_migrate_entries
|
| 20 | +from homeassistant.helpers.event import ( |
| 21 | + async_track_time_change, |
| 22 | + async_track_time_interval, |
| 23 | +) |
| 24 | +from homeassistant.helpers.dispatcher import ( |
| 25 | + async_dispatcher_connect, |
| 26 | + async_dispatcher_send, |
| 27 | +) |
16 | 28 | from homeassistant.const import (
|
17 | 29 | CONF_ADDRESS,
|
18 | 30 | CONF_COUNTRY,
|
| 31 | + CONF_HOST, |
19 | 32 | CONF_MODEL,
|
| 33 | + CONF_NAME, |
20 | 34 | CONF_PASSWORD,
|
21 | 35 | CONF_USERNAME,
|
22 | 36 | EVENT_HOMEASSISTANT_STARTED,
|
|
32 | 46 | ZenggeManager,
|
33 | 47 | )
|
34 | 48 | from .lib.ble.device import (
|
35 |
| - close_stale_connections, |
36 |
| - get_device, |
37 | 49 | UNILED_TRANSPORT_BLE,
|
38 | 50 | UniledBleDevice,
|
| 51 | + close_stale_connections, |
| 52 | + get_device, |
39 | 53 | )
|
40 | 54 | from .lib.net.device import (
|
41 | 55 | UNILED_TRANSPORT_NET,
|
42 | 56 | UniledNetDevice,
|
43 | 57 | )
|
| 58 | +from .discovery import ( |
| 59 | + UniledDiscovery, |
| 60 | + async_build_cached_discovery, |
| 61 | + async_clear_discovery_cache, |
| 62 | + async_get_discovery, |
| 63 | + async_discover_device, |
| 64 | + async_discover_devices, |
| 65 | + async_trigger_discovery, |
| 66 | + async_update_entry_from_discovery, |
| 67 | +) |
44 | 68 | from .const import (
|
45 | 69 | DOMAIN,
|
| 70 | + ATTR_UL_MAC_ADDRESS, |
46 | 71 | CONF_UL_RETRY_COUNT as CONF_RETRY_COUNT,
|
47 | 72 | CONF_UL_TRANSPORT as CONF_TRANSPORT,
|
48 | 73 | CONF_UL_UPDATE_INTERVAL as CONF_UPDATE_INTERVAL,
|
|
51 | 76 | UNILED_UPDATE_SECONDS as DEFAULT_UPDATE_INTERVAL,
|
52 | 77 | UNILED_DEVICE_TIMEOUT,
|
53 | 78 | UNILED_OPTIONS_ATTRIBUTES,
|
| 79 | + UNILED_DISCOVERY, |
| 80 | + UNILED_DISCOVERY_SIGNAL, |
| 81 | + UNILED_DISCOVERY_INTERVAL, |
| 82 | + UNILED_DISCOVERY_STARTUP_TIMEOUT, |
| 83 | + UNILED_DISCOVERY_SCAN_TIMEOUT, |
| 84 | + # UNILED_SIGNAL_STATE_UPDATED, |
54 | 85 | )
|
55 | 86 |
|
56 | 87 | from .coordinator import UniledUpdateCoordinator
|
|
73 | 104 | ]
|
74 | 105 |
|
75 | 106 |
|
| 107 | +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: |
| 108 | + """Set up the UNILED component.""" |
| 109 | + domain_data = hass.data.setdefault(DOMAIN, {}) |
| 110 | + domain_data[UNILED_DISCOVERY] = await async_discover_devices( |
| 111 | + hass, UNILED_DISCOVERY_STARTUP_TIMEOUT |
| 112 | + ) |
| 113 | + |
| 114 | + @callback |
| 115 | + def _async_start_background_discovery(*_: Any) -> None: |
| 116 | + """Run discovery in the background.""" |
| 117 | + hass.async_create_background_task(_async_discovery(), UNILED_DISCOVERY) |
| 118 | + |
| 119 | + async def _async_discovery(*_: Any) -> None: |
| 120 | + async_trigger_discovery( |
| 121 | + hass, await async_discover_devices(hass, UNILED_DISCOVERY_SCAN_TIMEOUT) |
| 122 | + ) |
| 123 | + |
| 124 | + async_trigger_discovery(hass, domain_data[UNILED_DISCOVERY]) |
| 125 | + |
| 126 | + hass.bus.async_listen_once( |
| 127 | + EVENT_HOMEASSISTANT_STARTED, _async_start_background_discovery |
| 128 | + ) |
| 129 | + |
| 130 | + async_track_time_interval( |
| 131 | + hass, |
| 132 | + _async_start_background_discovery, |
| 133 | + UNILED_DISCOVERY_INTERVAL, |
| 134 | + cancel_on_shutdown=True, |
| 135 | + ) |
| 136 | + return True |
| 137 | + |
| 138 | + |
76 | 139 | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
77 | 140 | """Set up UNILED from a config entry."""
|
78 | 141 | transport: str = entry.data.get(CONF_TRANSPORT)
|
@@ -223,12 +286,18 @@ def _async_update_ble(
|
223 | 286 | )
|
224 | 287 |
|
225 | 288 | elif transport == UNILED_TRANSPORT_NET:
|
226 |
| - raise ConfigEntryError( |
227 |
| - f"Unable to communicate with network device {address} as currently not supported!" |
228 |
| - ) |
| 289 | + discovery_cached = True |
| 290 | + host = entry.data[CONF_HOST] |
| 291 | + if discovery := async_get_discovery(hass, host): |
| 292 | + discovery_cached = False |
| 293 | + else: |
| 294 | + discovery = async_build_cached_discovery(entry) |
| 295 | + uniled = UniledNetDevice(discovery=discovery, options=entry.options) |
| 296 | + if not uniled.model: |
| 297 | + raise ConfigEntryError(f"Could not resolve model for device {host}") |
229 | 298 | else:
|
230 | 299 | raise ConfigEntryError(
|
231 |
| - f"Unable to communicate with device {address} of unsupported class: {transport}" |
| 300 | + f"Unable to communicate with device of unknown transport class: {transport}" |
232 | 301 | )
|
233 | 302 |
|
234 | 303 | coordinator = UniledUpdateCoordinator(hass, uniled, entry)
|
@@ -283,6 +352,44 @@ async def _async_stop(event: Event) -> None:
|
283 | 352 | hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop)
|
284 | 353 | )
|
285 | 354 |
|
| 355 | + if transport == UNILED_TRANSPORT_NET: |
| 356 | + # UDP probe after successful connect only |
| 357 | + if discovery_cached: |
| 358 | + if directed_discovery := await async_discover_device(hass, host): |
| 359 | + uniled.discovery = discovery = directed_discovery |
| 360 | + discovery_cached = False |
| 361 | + |
| 362 | + if entry.unique_id and discovery.get(ATTR_UL_MAC_ADDRESS): |
| 363 | + mac = uniled.format_mac(cast(str, discovery[ATTR_UL_MAC_ADDRESS])) |
| 364 | + if not uniled.mac_matches_by_one(mac, entry.unique_id): |
| 365 | + # The device is offline and another device is now using the ip address |
| 366 | + raise ConfigEntryNotReady( |
| 367 | + f"Unexpected device found at {host}; Expected {entry.unique_id}, found" |
| 368 | + f" {mac}" |
| 369 | + ) |
| 370 | + |
| 371 | + if not discovery_cached: |
| 372 | + # Only update the entry once we have verified the unique id |
| 373 | + # is either missing or we have verified it matches |
| 374 | + async_update_entry_from_discovery( |
| 375 | + hass, entry, discovery, uniled.model_name, True |
| 376 | + ) |
| 377 | + |
| 378 | + async def _async_handle_discovered_device() -> None: |
| 379 | + """Handle device discovery.""" |
| 380 | + # Force a refresh if the device is now available |
| 381 | + if not coordinator.last_update_success: |
| 382 | + coordinator.force_next_update = True |
| 383 | + await coordinator.async_refresh() |
| 384 | + |
| 385 | + entry.async_on_unload( |
| 386 | + async_dispatcher_connect( |
| 387 | + hass, |
| 388 | + UNILED_DISCOVERY_SIGNAL.format(entry_id=entry.entry_id), |
| 389 | + _async_handle_discovered_device, |
| 390 | + ) |
| 391 | + ) |
| 392 | + |
286 | 393 | _LOGGER.debug(
|
287 | 394 | "*** Added UniLED device entry for: %s, ID: %s, Unique ID: %s",
|
288 | 395 | uniled.name,
|
@@ -340,6 +447,9 @@ async def async_unload_entry(hass, entry) -> bool:
|
340 | 447 | if coordinator:
|
341 | 448 | if coordinator.device.transport != UNILED_TRANSPORT_NET:
|
342 | 449 | bluetooth.async_rediscover_address(hass, coordinator.device.address)
|
| 450 | + elif coordinator.device.transport == UNILED_TRANSPORT_NET: |
| 451 | + # Make sure we probe the device again in case something has changed externally |
| 452 | + async_clear_discovery_cache(hass, entry.data[CONF_HOST]) |
343 | 453 | del coordinator
|
344 | 454 | gc.collect()
|
345 | 455 |
|
@@ -371,5 +481,5 @@ async def async_migrate_entry(hass, entry):
|
371 | 481 | break
|
372 | 482 | entry.version = 3
|
373 | 483 | _LOGGER.info("Migration to version %s successful", entry.version)
|
374 |
| - |
| 484 | + |
375 | 485 | return True
|
0 commit comments