Skip to content

Commit 9d8b6ff

Browse files
committed
Refactor to support network devices
1 parent d217f79 commit 9d8b6ff

40 files changed

+4115
-368
lines changed

.gitignore

+5-1
Original file line numberDiff line numberDiff line change
@@ -138,4 +138,8 @@ custom_components/uniled/__pycache__
138138
custom_components/zengge_mesh
139139
custom_components/uniled/significant_change.py
140140
custom_components/uniled/reproduce_state.py
141-
.gitignore
141+
.gitignore
142+
tests/dump.txt
143+
tests/sok.py
144+
custom_components/__pycache__
145+
custom_components/uniled/main.py

README.md

+5-3
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
[![GitHub Activity][commits-shield]][commits]
55
[![License][license-shield]][license]
66

7-
# ![HA][ha-logo] UniLED v2.2.5 - The Universal Light Controller
7+
# ![HA][ha-logo] UniLED - The Universal Light Controller
88

9-
### UniLED supports the following range of BLE LED controllers:
9+
### UniLED supports the following range of BLE/WiFi LED controllers:
1010

1111
### 📱LED Chord
1212
- **SP107E** - SPI RGB(W) Controller
@@ -24,7 +24,7 @@
2424
- **SP621E** - Mini SPI RGB Controller
2525
- **SP623E** - Mini PWM RGB Controller
2626
- **SP624E** - Mini PWM RGBW Controller
27-
- **SP630E** - PWM/SPI RGB, RGBW, RGBCCT Controller
27+
- **SP530E** / **SP630E** - PWM/SPI RGB, RGBW, RGBCCT Controller
2828
- **SP631E** / **SP641E** - PWM Single Color Controllers
2929
- **SP632E** / **SP642E** - PWM CCT Controllers
3030
- **SP633E** / **SP643E** - PWM RGB Controllers
@@ -35,6 +35,8 @@
3535
- **SP638E** / **SP648E** - SPI RGB Controllers
3636
- **SP639E** / **SP649E** - SPI RGBW Controllers
3737
- **SP63AE** / **SP64AE** - SPI RGBCCT Controllers
38+
- **SP63BE** / **SP64BE** - SPI RGB+1CH PWM Controllers
39+
- **SP63CE** / **SP64CE** - SPI RGB+2CH PWM Controllers
3840

3941
#### 💡Hints and Tips
4042
1. For those devices that support "Effect Length", set the length to the number of LEDS.

custom_components/uniled/__init__.py

+117-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
"""The UniLED integration."""
2+
23
from __future__ import annotations
34

5+
from typing import Any, Final, cast
6+
47
from homeassistant.components import bluetooth
58
from homeassistant.components.bluetooth.match import (
69
BluetoothCallbackMatcher,
@@ -11,12 +14,23 @@
1114
from homeassistant.core import Event, HomeAssistant, callback, CoreState
1215
from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
1316
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
17+
from homeassistant.helpers.typing import ConfigType
1418
from homeassistant.helpers.device_registry import format_mac
1519
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+
)
1628
from homeassistant.const import (
1729
CONF_ADDRESS,
1830
CONF_COUNTRY,
31+
CONF_HOST,
1932
CONF_MODEL,
33+
CONF_NAME,
2034
CONF_PASSWORD,
2135
CONF_USERNAME,
2236
EVENT_HOMEASSISTANT_STARTED,
@@ -32,17 +46,28 @@
3246
ZenggeManager,
3347
)
3448
from .lib.ble.device import (
35-
close_stale_connections,
36-
get_device,
3749
UNILED_TRANSPORT_BLE,
3850
UniledBleDevice,
51+
close_stale_connections,
52+
get_device,
3953
)
4054
from .lib.net.device import (
4155
UNILED_TRANSPORT_NET,
4256
UniledNetDevice,
4357
)
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+
)
4468
from .const import (
4569
DOMAIN,
70+
ATTR_UL_MAC_ADDRESS,
4671
CONF_UL_RETRY_COUNT as CONF_RETRY_COUNT,
4772
CONF_UL_TRANSPORT as CONF_TRANSPORT,
4873
CONF_UL_UPDATE_INTERVAL as CONF_UPDATE_INTERVAL,
@@ -51,6 +76,12 @@
5176
UNILED_UPDATE_SECONDS as DEFAULT_UPDATE_INTERVAL,
5277
UNILED_DEVICE_TIMEOUT,
5378
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,
5485
)
5586

5687
from .coordinator import UniledUpdateCoordinator
@@ -73,6 +104,38 @@
73104
]
74105

75106

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+
76139
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
77140
"""Set up UNILED from a config entry."""
78141
transport: str = entry.data.get(CONF_TRANSPORT)
@@ -223,12 +286,18 @@ def _async_update_ble(
223286
)
224287

225288
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}")
229298
else:
230299
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}"
232301
)
233302

234303
coordinator = UniledUpdateCoordinator(hass, uniled, entry)
@@ -283,6 +352,44 @@ async def _async_stop(event: Event) -> None:
283352
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop)
284353
)
285354

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+
286393
_LOGGER.debug(
287394
"*** Added UniLED device entry for: %s, ID: %s, Unique ID: %s",
288395
uniled.name,
@@ -340,6 +447,9 @@ async def async_unload_entry(hass, entry) -> bool:
340447
if coordinator:
341448
if coordinator.device.transport != UNILED_TRANSPORT_NET:
342449
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])
343453
del coordinator
344454
gc.collect()
345455

@@ -371,5 +481,5 @@ async def async_migrate_entry(hass, entry):
371481
break
372482
entry.version = 3
373483
_LOGGER.info("Migration to version %s successful", entry.version)
374-
484+
375485
return True

0 commit comments

Comments
 (0)