1 """The Flux LED/MagicLight integration."""
3 from __future__
import annotations
5 from datetime
import timedelta
7 from typing
import Any, Final, cast
9 from flux_led
import DeviceType
10 from flux_led.aio
import AIOWifiLedBulb
11 from flux_led.const
import ATTR_ID, WhiteChannelType
12 from flux_led.scanner
import FluxLEDDiscovery
19 config_validation
as cv,
20 device_registry
as dr,
21 entity_registry
as er,
24 async_dispatcher_connect,
25 async_dispatcher_send,
28 async_track_time_change,
29 async_track_time_interval,
34 CONF_WHITE_CHANNEL_TYPE,
35 DISCOVER_SCAN_TIMEOUT,
38 FLUX_LED_DISCOVERY_SIGNAL,
42 from .coordinator
import FluxLedUpdateCoordinator
43 from .discovery
import (
44 async_build_cached_discovery,
45 async_clear_discovery_cache,
46 async_discover_device,
47 async_discover_devices,
49 async_trigger_discovery,
50 async_update_entry_from_discovery,
52 from .util
import mac_matches_by_one
54 _LOGGER = logging.getLogger(__name__)
56 PLATFORMS_BY_TYPE: Final = {
73 REQUEST_REFRESH_DELAY: Final = 1.5
74 NAME_TO_WHITE_CHANNEL_TYPE: Final = {
75 option.name.lower(): option
for option
in WhiteChannelType
78 CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
83 host: str, discovery: FluxLEDDiscovery |
None
85 """Create a AIOWifiLedBulb from a host."""
86 return AIOWifiLedBulb(host, discovery=discovery)
89 async
def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
90 """Set up the flux_led component."""
91 domain_data = hass.data.setdefault(DOMAIN, {})
92 domain_data[FLUX_LED_DISCOVERY] = []
95 def _async_start_background_discovery(*_: Any) ->
None:
96 """Run discovery in the background."""
97 hass.async_create_background_task(
98 _async_discovery(),
"flux_led-discovery", eager_start=
True
101 async
def _async_discovery(*_: Any) ->
None:
106 _async_start_background_discovery()
109 _async_start_background_discovery,
111 cancel_on_shutdown=
True,
117 """Migrate entities when the mac address gets discovered."""
120 def _async_migrator(entity_entry: er.RegistryEntry) -> dict[str, Any] |
None:
121 if not (unique_id := entry.unique_id):
123 entry_id = entry.entry_id
124 entity_unique_id = entity_entry.unique_id
125 entity_mac = entity_unique_id[: len(unique_id)]
127 if entity_unique_id.startswith(entry_id):
129 new_unique_id = f
"{unique_id}{entity_unique_id.removeprefix(entry_id)}"
132 and entity_mac != unique_id
136 new_unique_id = f
"{unique_id}{entity_unique_id[len(unique_id):]}"
140 "Migrating unique_id from [%s] to [%s]",
144 return {
"new_unique_id": new_unique_id}
146 await er.async_migrate_entries(hass, entry.entry_id, _async_migrator)
150 """Handle options update."""
151 coordinator: FluxLedUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
152 if entry.title != coordinator.title:
153 await hass.config_entries.async_reload(entry.entry_id)
157 """Set up Flux LED/MagicLight from a config entry."""
158 host = entry.data[CONF_HOST]
159 discovery_cached =
True
161 discovery_cached =
False
165 signal = SIGNAL_STATE_UPDATED.format(device.ipaddr)
166 device.discovery = discovery
167 if white_channel_type := entry.data.get(CONF_WHITE_CHANNEL_TYPE):
168 device.white_channel_channel_type = NAME_TO_WHITE_CHANNEL_TYPE[
173 def _async_state_changed(*_: Any) ->
None:
174 _LOGGER.debug(
"%s: Device state updated: %s", device.ipaddr, device.raw_state)
178 await device.async_setup(_async_state_changed)
179 except FLUX_LED_EXCEPTIONS
as ex:
181 str(ex)
or f
"Timed out trying to connect to {device.ipaddr}"
187 device.discovery = discovery = directed_discovery
188 discovery_cached =
False
190 if entry.unique_id
and discovery.get(ATTR_ID):
191 mac = dr.format_mac(cast(str, discovery[ATTR_ID]))
195 f
"Unexpected device found at {host}; Expected {entry.unique_id}, found"
199 if not discovery_cached:
203 hass, entry, discovery, device.model_num,
True
209 hass.data[DOMAIN][entry.entry_id] = coordinator
210 platforms = PLATFORMS_BY_TYPE[device.device_type]
211 await hass.config_entries.async_forward_entry_setups(entry, platforms)
213 async
def _async_sync_time(*args: Any) ->
None:
214 """Set the time every morning at 02:40:30."""
215 await device.async_set_time()
217 await _async_sync_time()
223 entry.async_on_unload(entry.add_update_listener(_async_update_listener))
225 async
def _async_handle_discovered_device() -> None:
226 """Handle device discovery."""
228 if not coordinator.last_update_success:
229 coordinator.force_next_update =
True
230 await coordinator.async_refresh()
232 entry.async_on_unload(
235 FLUX_LED_DISCOVERY_SIGNAL.format(entry_id=entry.entry_id),
236 _async_handle_discovered_device,
243 """Unload a config entry."""
244 device: AIOWifiLedBulb = hass.data[DOMAIN][entry.entry_id].device
245 platforms = PLATFORMS_BY_TYPE[device.device_type]
246 if unload_ok := await hass.config_entries.async_unload_platforms(entry, platforms):
249 del hass.data[DOMAIN][entry.entry_id]
250 await device.async_stop()
ElkSystem|None async_discover_device(HomeAssistant hass, str host)
bool async_update_entry_from_discovery(HomeAssistant hass, config_entries.ConfigEntry entry, ElkSystem device)
None async_clear_discovery_cache(HomeAssistant hass, str host)
FluxLEDDiscovery async_build_cached_discovery(ConfigEntry entry)
FluxLEDDiscovery|None async_get_discovery(HomeAssistant hass, str host)
bool mac_matches_by_one(str formatted_mac_1, str formatted_mac_2)
None _async_migrate_unique_ids(HomeAssistant hass, ConfigEntry entry)
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
bool async_setup(HomeAssistant hass, ConfigType config)
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
None _async_update_listener(HomeAssistant hass, ConfigEntry entry)
AIOWifiLedBulb async_wifi_bulb_for_host(str host, FluxLEDDiscovery|None discovery)
dict[str, Device] async_discover_devices(HomeAssistant hass)
None async_trigger_discovery(HomeAssistant hass, dict[str, Device] discovered_devices)
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)
CALLBACK_TYPE async_track_time_change(HomeAssistant hass, Callable[[datetime], Coroutine[Any, Any, None]|None] action, Any|None hour=None, Any|None minute=None, Any|None second=None)
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)