Skip to content

Commit

Permalink
Refactor to support network devices
Browse files Browse the repository at this point in the history
  • Loading branch information
monty68 committed Dec 1, 2024
1 parent d217f79 commit 9d8b6ff
Show file tree
Hide file tree
Showing 40 changed files with 4,115 additions and 368 deletions.
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -138,4 +138,8 @@ custom_components/uniled/__pycache__
custom_components/zengge_mesh
custom_components/uniled/significant_change.py
custom_components/uniled/reproduce_state.py
.gitignore
.gitignore
tests/dump.txt
tests/sok.py
custom_components/__pycache__
custom_components/uniled/main.py
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
[![GitHub Activity][commits-shield]][commits]
[![License][license-shield]][license]

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

### UniLED supports the following range of BLE LED controllers:
### UniLED supports the following range of BLE/WiFi LED controllers:

### 📱LED Chord
- **SP107E** - SPI RGB(W) Controller
Expand All @@ -24,7 +24,7 @@
- **SP621E** - Mini SPI RGB Controller
- **SP623E** - Mini PWM RGB Controller
- **SP624E** - Mini PWM RGBW Controller
- **SP630E** - PWM/SPI RGB, RGBW, RGBCCT Controller
- **SP530E** / **SP630E** - PWM/SPI RGB, RGBW, RGBCCT Controller
- **SP631E** / **SP641E** - PWM Single Color Controllers
- **SP632E** / **SP642E** - PWM CCT Controllers
- **SP633E** / **SP643E** - PWM RGB Controllers
Expand All @@ -35,6 +35,8 @@
- **SP638E** / **SP648E** - SPI RGB Controllers
- **SP639E** / **SP649E** - SPI RGBW Controllers
- **SP63AE** / **SP64AE** - SPI RGBCCT Controllers
- **SP63BE** / **SP64BE** - SPI RGB+1CH PWM Controllers
- **SP63CE** / **SP64CE** - SPI RGB+2CH PWM Controllers

#### 💡Hints and Tips
1. For those devices that support "Effect Length", set the length to the number of LEDS.
Expand Down
124 changes: 117 additions & 7 deletions custom_components/uniled/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
"""The UniLED integration."""

from __future__ import annotations

from typing import Any, Final, cast

from homeassistant.components import bluetooth
from homeassistant.components.bluetooth.match import (
BluetoothCallbackMatcher,
Expand All @@ -11,12 +14,23 @@
from homeassistant.core import Event, HomeAssistant, callback, CoreState
from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.entity_registry import async_get, async_migrate_entries
from homeassistant.helpers.event import (
async_track_time_change,
async_track_time_interval,
)
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
async_dispatcher_send,
)
from homeassistant.const import (
CONF_ADDRESS,
CONF_COUNTRY,
CONF_HOST,
CONF_MODEL,
CONF_NAME,
CONF_PASSWORD,
CONF_USERNAME,
EVENT_HOMEASSISTANT_STARTED,
Expand All @@ -32,17 +46,28 @@
ZenggeManager,
)
from .lib.ble.device import (
close_stale_connections,
get_device,
UNILED_TRANSPORT_BLE,
UniledBleDevice,
close_stale_connections,
get_device,
)
from .lib.net.device import (
UNILED_TRANSPORT_NET,
UniledNetDevice,
)
from .discovery import (
UniledDiscovery,
async_build_cached_discovery,
async_clear_discovery_cache,
async_get_discovery,
async_discover_device,
async_discover_devices,
async_trigger_discovery,
async_update_entry_from_discovery,
)
from .const import (
DOMAIN,
ATTR_UL_MAC_ADDRESS,
CONF_UL_RETRY_COUNT as CONF_RETRY_COUNT,
CONF_UL_TRANSPORT as CONF_TRANSPORT,
CONF_UL_UPDATE_INTERVAL as CONF_UPDATE_INTERVAL,
Expand All @@ -51,6 +76,12 @@
UNILED_UPDATE_SECONDS as DEFAULT_UPDATE_INTERVAL,
UNILED_DEVICE_TIMEOUT,
UNILED_OPTIONS_ATTRIBUTES,
UNILED_DISCOVERY,
UNILED_DISCOVERY_SIGNAL,
UNILED_DISCOVERY_INTERVAL,
UNILED_DISCOVERY_STARTUP_TIMEOUT,
UNILED_DISCOVERY_SCAN_TIMEOUT,
# UNILED_SIGNAL_STATE_UPDATED,
)

from .coordinator import UniledUpdateCoordinator
Expand All @@ -73,6 +104,38 @@
]


async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the UNILED component."""
domain_data = hass.data.setdefault(DOMAIN, {})
domain_data[UNILED_DISCOVERY] = await async_discover_devices(
hass, UNILED_DISCOVERY_STARTUP_TIMEOUT
)

@callback
def _async_start_background_discovery(*_: Any) -> None:
"""Run discovery in the background."""
hass.async_create_background_task(_async_discovery(), UNILED_DISCOVERY)

async def _async_discovery(*_: Any) -> None:
async_trigger_discovery(
hass, await async_discover_devices(hass, UNILED_DISCOVERY_SCAN_TIMEOUT)
)

async_trigger_discovery(hass, domain_data[UNILED_DISCOVERY])

hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STARTED, _async_start_background_discovery
)

async_track_time_interval(
hass,
_async_start_background_discovery,
UNILED_DISCOVERY_INTERVAL,
cancel_on_shutdown=True,
)
return True


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up UNILED from a config entry."""
transport: str = entry.data.get(CONF_TRANSPORT)
Expand Down Expand Up @@ -223,12 +286,18 @@ def _async_update_ble(
)

elif transport == UNILED_TRANSPORT_NET:
raise ConfigEntryError(
f"Unable to communicate with network device {address} as currently not supported!"
)
discovery_cached = True
host = entry.data[CONF_HOST]
if discovery := async_get_discovery(hass, host):
discovery_cached = False
else:
discovery = async_build_cached_discovery(entry)
uniled = UniledNetDevice(discovery=discovery, options=entry.options)
if not uniled.model:
raise ConfigEntryError(f"Could not resolve model for device {host}")
else:
raise ConfigEntryError(
f"Unable to communicate with device {address} of unsupported class: {transport}"
f"Unable to communicate with device of unknown transport class: {transport}"
)

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

if transport == UNILED_TRANSPORT_NET:
# UDP probe after successful connect only
if discovery_cached:
if directed_discovery := await async_discover_device(hass, host):
uniled.discovery = discovery = directed_discovery
discovery_cached = False

if entry.unique_id and discovery.get(ATTR_UL_MAC_ADDRESS):
mac = uniled.format_mac(cast(str, discovery[ATTR_UL_MAC_ADDRESS]))
if not uniled.mac_matches_by_one(mac, entry.unique_id):
# The device is offline and another device is now using the ip address
raise ConfigEntryNotReady(
f"Unexpected device found at {host}; Expected {entry.unique_id}, found"
f" {mac}"
)

if not discovery_cached:
# Only update the entry once we have verified the unique id
# is either missing or we have verified it matches
async_update_entry_from_discovery(
hass, entry, discovery, uniled.model_name, True
)

async def _async_handle_discovered_device() -> None:
"""Handle device discovery."""
# Force a refresh if the device is now available
if not coordinator.last_update_success:
coordinator.force_next_update = True
await coordinator.async_refresh()

entry.async_on_unload(
async_dispatcher_connect(
hass,
UNILED_DISCOVERY_SIGNAL.format(entry_id=entry.entry_id),
_async_handle_discovered_device,
)
)

_LOGGER.debug(
"*** Added UniLED device entry for: %s, ID: %s, Unique ID: %s",
uniled.name,
Expand Down Expand Up @@ -340,6 +447,9 @@ async def async_unload_entry(hass, entry) -> bool:
if coordinator:
if coordinator.device.transport != UNILED_TRANSPORT_NET:
bluetooth.async_rediscover_address(hass, coordinator.device.address)
elif coordinator.device.transport == UNILED_TRANSPORT_NET:
# Make sure we probe the device again in case something has changed externally
async_clear_discovery_cache(hass, entry.data[CONF_HOST])
del coordinator
gc.collect()

Expand Down Expand Up @@ -371,5 +481,5 @@ async def async_migrate_entry(hass, entry):
break
entry.version = 3
_LOGGER.info("Migration to version %s successful", entry.version)

return True
Loading

0 comments on commit 9d8b6ff

Please sign in to comment.