Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
77bd953
Add minor version to device info reporting
bdraco Dec 7, 2021
4695d18
fixes
bdraco Dec 7, 2021
3098ceb
adjust
bdraco Dec 7, 2021
bad4c66
remote access fixes
bdraco Dec 11, 2021
729a796
fix tests
bdraco Dec 11, 2021
9134932
bump
bdraco Dec 11, 2021
2d22cb8
merge
bdraco Dec 11, 2021
8af987c
merge
bdraco Dec 11, 2021
c59a4fa
adjust
bdraco Dec 11, 2021
d012e8a
prune
bdraco Dec 11, 2021
419a203
tweaks
bdraco Dec 11, 2021
bafd7f5
back to nothing awaited
bdraco Dec 11, 2021
12b7b2d
back to nothing awaited
bdraco Dec 11, 2021
70dacac
handle wrong device after dhcp res change
bdraco Dec 11, 2021
3f0492d
tweaks
bdraco Dec 11, 2021
325ffa9
adjust
bdraco Dec 11, 2021
540c4ef
adjust
bdraco Dec 11, 2021
eab0873
adjust
bdraco Dec 11, 2021
a574260
tweaks
bdraco Dec 11, 2021
591b26e
icon
bdraco Dec 11, 2021
c169823
adjust
bdraco Dec 11, 2021
2e866e2
fix
bdraco Dec 11, 2021
92c3b53
merge
bdraco Dec 11, 2021
02e45c0
cover
bdraco Dec 11, 2021
47d5024
do not merge none
bdraco Dec 11, 2021
8d68c96
adjust
bdraco Dec 11, 2021
dbd8311
fix
bdraco Dec 11, 2021
77258c4
merge
bdraco Dec 11, 2021
d9129cd
merge
bdraco Dec 11, 2021
290d5c2
merge
bdraco Dec 12, 2021
0ff9fed
tests
bdraco Dec 12, 2021
224c150
fix
bdraco Dec 12, 2021
6109bf1
merge
bdraco Dec 12, 2021
f82f56f
logic reversed
bdraco Dec 12, 2021
b2cdac5
ensure model is saved
bdraco Dec 13, 2021
0a891ef
rebase to include hw_version
bdraco Dec 16, 2021
393afa2
bump version
bdraco Dec 18, 2021
a5ed0e4
Merge branch 'dev' into flux_led_minor_version
bdraco Dec 19, 2021
21bb45c
Merge
bdraco Dec 19, 2021
fa590d4
fix merge
bdraco Dec 19, 2021
9dc8ef2
fix test
bdraco Dec 19, 2021
3b301d4
adjust tests
bdraco Dec 19, 2021
c72f2db
Update homeassistant/components/flux_led/switch.py
bdraco Dec 19, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 33 additions & 41 deletions homeassistant/components/flux_led/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,14 @@

from datetime import timedelta
import logging
from typing import Any, Final
from typing import Any, Final, cast

from flux_led import DeviceType
from flux_led.aio import AIOWifiLedBulb
from flux_led.const import ATTR_ID
from flux_led.scanner import FluxLEDDiscovery

from homeassistant import config_entries
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_HOST,
CONF_NAME,
EVENT_HOMEASSISTANT_STARTED,
Platform,
)
from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STARTED, Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
Expand All @@ -36,16 +29,18 @@
STARTUP_SCAN_TIMEOUT,
)
from .discovery import (
async_clear_discovery_cache,
async_discover_device,
async_discover_devices,
async_name_from_discovery,
async_get_discovery,
async_trigger_discovery,
async_update_entry_from_discovery,
)

_LOGGER = logging.getLogger(__name__)

PLATFORMS_BY_TYPE: Final = {
DeviceType.Bulb: [Platform.LIGHT, Platform.NUMBER],
DeviceType.Bulb: [Platform.LIGHT, Platform.NUMBER, Platform.SWITCH],
DeviceType.Switch: [Platform.SWITCH],
}
DISCOVERY_INTERVAL: Final = timedelta(minutes=15)
Expand All @@ -58,22 +53,6 @@ def async_wifi_bulb_for_host(host: str) -> AIOWifiLedBulb:
return AIOWifiLedBulb(host)


@callback
def async_update_entry_from_discovery(
hass: HomeAssistant, entry: config_entries.ConfigEntry, device: FluxLEDDiscovery
) -> None:
"""Update a config entry from a flux_led discovery."""
name = async_name_from_discovery(device)
mac_address = device[ATTR_ID]
assert mac_address is not None
hass.config_entries.async_update_entry(
entry,
data={**entry.data, CONF_NAME: name},
title=name,
unique_id=dr.format_mac(mac_address),
)


async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the flux_led component."""
domain_data = hass.data.setdefault(DOMAIN, {})
Expand All @@ -92,18 +71,9 @@ async def _async_discovery(*_: Any) -> None:
return True


async def async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Update listener."""
await hass.config_entries.async_reload(entry.entry_id)


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Flux LED/MagicLight from a config entry."""
host = entry.data[CONF_HOST]
if not entry.unique_id:
if discovery := await async_discover_device(hass, host):
async_update_entry_from_discovery(hass, entry, discovery)

device: AIOWifiLedBulb = async_wifi_bulb_for_host(host)
signal = SIGNAL_STATE_UPDATED.format(device.ipaddr)

Expand All @@ -119,11 +89,32 @@ def _async_state_changed(*_: Any) -> None:
str(ex) or f"Timed out trying to connect to {device.ipaddr}"
) from ex

coordinator = FluxLedUpdateCoordinator(hass, device)
# UDP probe after successful connect only
directed_discovery = None
if discovery := async_get_discovery(hass, host):
directed_discovery = False
elif discovery := await async_discover_device(hass, host):
directed_discovery = True

if discovery:
if entry.unique_id:
assert discovery[ATTR_ID] is not None
mac = dr.format_mac(cast(str, discovery[ATTR_ID]))
if mac != entry.unique_id:
# The device is offline and another flux_led device is now using the ip address
raise ConfigEntryNotReady(
f"Unexpected device found at {host}; Expected {entry.unique_id}, found {mac}"
)
if directed_discovery:
# 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)
device.discovery = discovery

coordinator = FluxLedUpdateCoordinator(hass, device, entry)
hass.data[DOMAIN][entry.entry_id] = coordinator
platforms = PLATFORMS_BY_TYPE[device.device_type]
hass.config_entries.async_setup_platforms(entry, platforms)
entry.async_on_unload(entry.add_update_listener(async_update_listener))

return True

Expand All @@ -133,6 +124,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
device: AIOWifiLedBulb = hass.data[DOMAIN][entry.entry_id].device
platforms = PLATFORMS_BY_TYPE[device.device_type]
if unload_ok := await hass.config_entries.async_unload_platforms(entry, platforms):
# Make sure we probe the device again in case something has changed externally
async_clear_discovery_cache(hass, entry.data[CONF_HOST])
del hass.data[DOMAIN][entry.entry_id]
await device.async_stop()
return unload_ok
Expand All @@ -142,12 +135,11 @@ class FluxLedUpdateCoordinator(DataUpdateCoordinator):
"""DataUpdateCoordinator to gather data for a specific flux_led device."""

def __init__(
self,
hass: HomeAssistant,
device: AIOWifiLedBulb,
self, hass: HomeAssistant, device: AIOWifiLedBulb, entry: ConfigEntry
) -> None:
"""Initialize DataUpdateCoordinator to gather data for specific device."""
self.device = device
self.entry = entry
super().__init__(
hass,
_LOGGER,
Expand Down
30 changes: 17 additions & 13 deletions homeassistant/components/flux_led/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@

from homeassistant import config_entries
from homeassistant.components import dhcp
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_MODE, CONF_NAME, CONF_PROTOCOL
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PROTOCOL
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.typing import DiscoveryInfoType

from . import async_update_entry_from_discovery, async_wifi_bulb_for_host
from . import async_wifi_bulb_for_host
from .const import (
CONF_CUSTOM_EFFECT_COLORS,
CONF_CUSTOM_EFFECT_SPEED_PCT,
Expand All @@ -33,6 +33,8 @@
async_discover_device,
async_discover_devices,
async_name_from_discovery,
async_populate_data_from_discovery,
async_update_entry_from_discovery,
)

CONF_DEVICE: Final = "device"
Expand Down Expand Up @@ -73,7 +75,6 @@ async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult:
CONF_PROTOCOL: user_input.get(CONF_PROTOCOL),
},
options={
CONF_MODE: user_input[CONF_MODE],
CONF_CUSTOM_EFFECT_COLORS: user_input[CONF_CUSTOM_EFFECT_COLORS],
CONF_CUSTOM_EFFECT_SPEED_PCT: user_input[CONF_CUSTOM_EFFECT_SPEED_PCT],
CONF_CUSTOM_EFFECT_TRANSITION: user_input[
Expand All @@ -86,7 +87,7 @@ async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowRes
"""Handle discovery via dhcp."""
self._discovered_device = FluxLEDDiscovery(
ipaddr=discovery_info.ip,
model=discovery_info.hostname,
model=None,
id=discovery_info.macaddress.replace(":", ""),
model_num=None,
version_num=None,
Expand Down Expand Up @@ -115,11 +116,12 @@ async def _async_handle_discovery(self) -> FlowResult:
mac = dr.format_mac(mac_address)
host = device[ATTR_IPADDR]
await self.async_set_unique_id(mac)
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
for entry in self._async_current_entries(include_ignore=False):
if entry.data[CONF_HOST] == host:
if not entry.unique_id:
async_update_entry_from_discovery(self.hass, entry, device)
if entry.unique_id == mac or entry.data[CONF_HOST] == host:
if async_update_entry_from_discovery(self.hass, entry, device):
self.hass.async_create_task(
self.hass.config_entries.async_reload(entry.entry_id)
)
return self.async_abort(reason="already_configured")
self.context[CONF_HOST] = host
for progress in self._async_in_progress():
Expand Down Expand Up @@ -164,12 +166,14 @@ def _async_create_entry_from_device(self, device: FluxLEDDiscovery) -> FlowResul
"""Create a config entry from a device."""
self._async_abort_entries_match({CONF_HOST: device[ATTR_IPADDR]})
name = async_name_from_discovery(device)
data: dict[str, Any] = {
CONF_HOST: device[ATTR_IPADDR],
CONF_NAME: name,
}
async_populate_data_from_discovery(data, data, device)
return self.async_create_entry(
title=name,
data={
CONF_HOST: device[ATTR_IPADDR],
CONF_NAME: name,
},
data=data,
)

async def async_step_user(
Expand Down Expand Up @@ -259,7 +263,7 @@ async def _async_try_connect(
model=model,
id=mac_address,
model_num=bulb.model_num,
version_num=bulb.version_num,
version_num=None, # This is the minor version number
firmware_date=None,
model_info=None,
model_description=bulb.model_data.description,
Expand Down
4 changes: 4 additions & 0 deletions homeassistant/components/flux_led/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@
CONF_DEVICES: Final = "devices"
CONF_CUSTOM_EFFECT: Final = "custom_effect"
CONF_MODEL: Final = "model"
CONF_MINOR_VERSION: Final = "minor_version"
CONF_REMOTE_ACCESS_ENABLED: Final = "remote_access_enabled"
CONF_REMOTE_ACCESS_HOST: Final = "remote_access_host"
CONF_REMOTE_ACCESS_PORT: Final = "remote_access_port"

MODE_AUTO: Final = "auto"
MODE_RGB: Final = "rgb"
Expand Down
95 changes: 92 additions & 3 deletions homeassistant/components/flux_led/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,54 @@
from __future__ import annotations

import asyncio
from collections.abc import Mapping
import logging
from typing import Any, Final

from flux_led.aioscanner import AIOBulbScanner
from flux_led.const import ATTR_ID, ATTR_IPADDR, ATTR_MODEL, ATTR_MODEL_DESCRIPTION
from flux_led.const import (
ATTR_ID,
ATTR_IPADDR,
ATTR_MODEL,
ATTR_MODEL_DESCRIPTION,
ATTR_REMOTE_ACCESS_ENABLED,
ATTR_REMOTE_ACCESS_HOST,
ATTR_REMOTE_ACCESS_PORT,
ATTR_VERSION_NUM,
)
from flux_led.scanner import FluxLEDDiscovery

from homeassistant import config_entries
from homeassistant.components import network
from homeassistant.const import CONF_HOST, CONF_NAME
from homeassistant.core import HomeAssistant, callback

from .const import DISCOVER_SCAN_TIMEOUT, DOMAIN
from homeassistant.helpers import device_registry as dr
from homeassistant.util.network import is_ip_address

from .const import (
CONF_MINOR_VERSION,
CONF_MODEL,
CONF_REMOTE_ACCESS_ENABLED,
CONF_REMOTE_ACCESS_HOST,
CONF_REMOTE_ACCESS_PORT,
DISCOVER_SCAN_TIMEOUT,
DOMAIN,
FLUX_LED_DISCOVERY,
)

_LOGGER = logging.getLogger(__name__)


CONF_TO_DISCOVERY: Final = {
CONF_HOST: ATTR_IPADDR,
CONF_REMOTE_ACCESS_ENABLED: ATTR_REMOTE_ACCESS_ENABLED,
CONF_REMOTE_ACCESS_HOST: ATTR_REMOTE_ACCESS_HOST,
CONF_REMOTE_ACCESS_PORT: ATTR_REMOTE_ACCESS_PORT,
CONF_MINOR_VERSION: ATTR_VERSION_NUM,
CONF_MODEL: ATTR_MODEL,
}


@callback
def async_name_from_discovery(device: FluxLEDDiscovery) -> str:
"""Convert a flux_led discovery to a human readable name."""
Expand All @@ -29,6 +62,62 @@ def async_name_from_discovery(device: FluxLEDDiscovery) -> str:
return f"{device[ATTR_MODEL]} {short_mac}"


@callback
def async_populate_data_from_discovery(
current_data: Mapping[str, Any],
data_updates: dict[str, Any],
device: FluxLEDDiscovery,
) -> None:
"""Copy discovery data into config entry data."""
for conf_key, discovery_key in CONF_TO_DISCOVERY.items():
if (
device.get(discovery_key) is not None
and current_data.get(conf_key) != device[discovery_key] # type: ignore[misc]
):
data_updates[conf_key] = device[discovery_key] # type: ignore[misc]


@callback
def async_update_entry_from_discovery(
hass: HomeAssistant, entry: config_entries.ConfigEntry, device: FluxLEDDiscovery
) -> bool:
"""Update a config entry from a flux_led discovery."""
data_updates: dict[str, Any] = {}
mac_address = device[ATTR_ID]
assert mac_address is not None
updates: dict[str, Any] = {}
if not entry.unique_id:
updates["unique_id"] = dr.format_mac(mac_address)
async_populate_data_from_discovery(entry.data, data_updates, device)
if not entry.data.get(CONF_NAME) or is_ip_address(entry.data[CONF_NAME]):
updates["title"] = data_updates[CONF_NAME] = async_name_from_discovery(device)
if data_updates:
updates["data"] = {**entry.data, **data_updates}
if updates:
return hass.config_entries.async_update_entry(entry, **updates)
return False


@callback
def async_get_discovery(hass: HomeAssistant, host: str) -> FluxLEDDiscovery | None:
"""Check if a device was already discovered via a broadcast discovery."""
discoveries: list[FluxLEDDiscovery] = hass.data[DOMAIN][FLUX_LED_DISCOVERY]
for discovery in discoveries:
if discovery[ATTR_IPADDR] == host:
return discovery
return None


@callback
def async_clear_discovery_cache(hass: HomeAssistant, host: str) -> None:
"""Clear the host from the discovery cache."""
domain_data = hass.data[DOMAIN]
discoveries: list[FluxLEDDiscovery] = domain_data[FLUX_LED_DISCOVERY]
domain_data[FLUX_LED_DISCOVERY] = [
discovery for discovery in discoveries if discovery[ATTR_IPADDR] != host
]


async def async_discover_devices(
hass: HomeAssistant, timeout: int, address: str | None = None
) -> list[FluxLEDDiscovery]:
Expand Down
Loading