Add support for enabling/disabling cloud access in flux_led (#61138)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
This commit is contained in:
parent
d7c5e41802
commit
a6b680cd32
13 changed files with 428 additions and 111 deletions
|
@ -3,21 +3,14 @@ from __future__ import annotations
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Final
|
from typing import Any, Final, cast
|
||||||
|
|
||||||
from flux_led import DeviceType
|
from flux_led import DeviceType
|
||||||
from flux_led.aio import AIOWifiLedBulb
|
from flux_led.aio import AIOWifiLedBulb
|
||||||
from flux_led.const import ATTR_ID
|
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.config_entries import ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STARTED, Platform
|
||||||
CONF_HOST,
|
|
||||||
CONF_NAME,
|
|
||||||
EVENT_HOMEASSISTANT_STARTED,
|
|
||||||
Platform,
|
|
||||||
)
|
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
from homeassistant.helpers import device_registry as dr
|
from homeassistant.helpers import device_registry as dr
|
||||||
|
@ -36,16 +29,18 @@ from .const import (
|
||||||
STARTUP_SCAN_TIMEOUT,
|
STARTUP_SCAN_TIMEOUT,
|
||||||
)
|
)
|
||||||
from .discovery import (
|
from .discovery import (
|
||||||
|
async_clear_discovery_cache,
|
||||||
async_discover_device,
|
async_discover_device,
|
||||||
async_discover_devices,
|
async_discover_devices,
|
||||||
async_name_from_discovery,
|
async_get_discovery,
|
||||||
async_trigger_discovery,
|
async_trigger_discovery,
|
||||||
|
async_update_entry_from_discovery,
|
||||||
)
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
PLATFORMS_BY_TYPE: Final = {
|
PLATFORMS_BY_TYPE: Final = {
|
||||||
DeviceType.Bulb: [Platform.LIGHT, Platform.NUMBER],
|
DeviceType.Bulb: [Platform.LIGHT, Platform.NUMBER, Platform.SWITCH],
|
||||||
DeviceType.Switch: [Platform.SWITCH],
|
DeviceType.Switch: [Platform.SWITCH],
|
||||||
}
|
}
|
||||||
DISCOVERY_INTERVAL: Final = timedelta(minutes=15)
|
DISCOVERY_INTERVAL: Final = timedelta(minutes=15)
|
||||||
|
@ -58,22 +53,6 @@ def async_wifi_bulb_for_host(host: str) -> AIOWifiLedBulb:
|
||||||
return AIOWifiLedBulb(host)
|
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:
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
"""Set up the flux_led component."""
|
"""Set up the flux_led component."""
|
||||||
domain_data = hass.data.setdefault(DOMAIN, {})
|
domain_data = hass.data.setdefault(DOMAIN, {})
|
||||||
|
@ -92,18 +71,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
return True
|
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:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Set up Flux LED/MagicLight from a config entry."""
|
"""Set up Flux LED/MagicLight from a config entry."""
|
||||||
host = entry.data[CONF_HOST]
|
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)
|
device: AIOWifiLedBulb = async_wifi_bulb_for_host(host)
|
||||||
signal = SIGNAL_STATE_UPDATED.format(device.ipaddr)
|
signal = SIGNAL_STATE_UPDATED.format(device.ipaddr)
|
||||||
|
|
||||||
|
@ -119,11 +89,32 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
str(ex) or f"Timed out trying to connect to {device.ipaddr}"
|
str(ex) or f"Timed out trying to connect to {device.ipaddr}"
|
||||||
) from ex
|
) 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
|
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||||
platforms = PLATFORMS_BY_TYPE[device.device_type]
|
platforms = PLATFORMS_BY_TYPE[device.device_type]
|
||||||
hass.config_entries.async_setup_platforms(entry, platforms)
|
hass.config_entries.async_setup_platforms(entry, platforms)
|
||||||
entry.async_on_unload(entry.add_update_listener(async_update_listener))
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -133,6 +124,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
device: AIOWifiLedBulb = hass.data[DOMAIN][entry.entry_id].device
|
device: AIOWifiLedBulb = hass.data[DOMAIN][entry.entry_id].device
|
||||||
platforms = PLATFORMS_BY_TYPE[device.device_type]
|
platforms = PLATFORMS_BY_TYPE[device.device_type]
|
||||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, platforms):
|
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]
|
del hass.data[DOMAIN][entry.entry_id]
|
||||||
await device.async_stop()
|
await device.async_stop()
|
||||||
return unload_ok
|
return unload_ok
|
||||||
|
@ -142,12 +135,11 @@ class FluxLedUpdateCoordinator(DataUpdateCoordinator):
|
||||||
"""DataUpdateCoordinator to gather data for a specific flux_led device."""
|
"""DataUpdateCoordinator to gather data for a specific flux_led device."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self, hass: HomeAssistant, device: AIOWifiLedBulb, entry: ConfigEntry
|
||||||
hass: HomeAssistant,
|
|
||||||
device: AIOWifiLedBulb,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize DataUpdateCoordinator to gather data for specific device."""
|
"""Initialize DataUpdateCoordinator to gather data for specific device."""
|
||||||
self.device = device
|
self.device = device
|
||||||
|
self.entry = entry
|
||||||
super().__init__(
|
super().__init__(
|
||||||
hass,
|
hass,
|
||||||
_LOGGER,
|
_LOGGER,
|
||||||
|
|
|
@ -10,13 +10,13 @@ import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.components import dhcp
|
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.core import callback
|
||||||
from homeassistant.data_entry_flow import FlowResult
|
from homeassistant.data_entry_flow import FlowResult
|
||||||
from homeassistant.helpers import device_registry as dr
|
from homeassistant.helpers import device_registry as dr
|
||||||
from homeassistant.helpers.typing import DiscoveryInfoType
|
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 (
|
from .const import (
|
||||||
CONF_CUSTOM_EFFECT_COLORS,
|
CONF_CUSTOM_EFFECT_COLORS,
|
||||||
CONF_CUSTOM_EFFECT_SPEED_PCT,
|
CONF_CUSTOM_EFFECT_SPEED_PCT,
|
||||||
|
@ -33,6 +33,8 @@ from .discovery import (
|
||||||
async_discover_device,
|
async_discover_device,
|
||||||
async_discover_devices,
|
async_discover_devices,
|
||||||
async_name_from_discovery,
|
async_name_from_discovery,
|
||||||
|
async_populate_data_from_discovery,
|
||||||
|
async_update_entry_from_discovery,
|
||||||
)
|
)
|
||||||
|
|
||||||
CONF_DEVICE: Final = "device"
|
CONF_DEVICE: Final = "device"
|
||||||
|
@ -73,7 +75,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
CONF_PROTOCOL: user_input.get(CONF_PROTOCOL),
|
CONF_PROTOCOL: user_input.get(CONF_PROTOCOL),
|
||||||
},
|
},
|
||||||
options={
|
options={
|
||||||
CONF_MODE: user_input[CONF_MODE],
|
|
||||||
CONF_CUSTOM_EFFECT_COLORS: user_input[CONF_CUSTOM_EFFECT_COLORS],
|
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_SPEED_PCT: user_input[CONF_CUSTOM_EFFECT_SPEED_PCT],
|
||||||
CONF_CUSTOM_EFFECT_TRANSITION: user_input[
|
CONF_CUSTOM_EFFECT_TRANSITION: user_input[
|
||||||
|
@ -86,7 +87,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
"""Handle discovery via dhcp."""
|
"""Handle discovery via dhcp."""
|
||||||
self._discovered_device = FluxLEDDiscovery(
|
self._discovered_device = FluxLEDDiscovery(
|
||||||
ipaddr=discovery_info.ip,
|
ipaddr=discovery_info.ip,
|
||||||
model=discovery_info.hostname,
|
model=None,
|
||||||
id=discovery_info.macaddress.replace(":", ""),
|
id=discovery_info.macaddress.replace(":", ""),
|
||||||
model_num=None,
|
model_num=None,
|
||||||
version_num=None,
|
version_num=None,
|
||||||
|
@ -115,11 +116,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
mac = dr.format_mac(mac_address)
|
mac = dr.format_mac(mac_address)
|
||||||
host = device[ATTR_IPADDR]
|
host = device[ATTR_IPADDR]
|
||||||
await self.async_set_unique_id(mac)
|
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):
|
for entry in self._async_current_entries(include_ignore=False):
|
||||||
if entry.data[CONF_HOST] == host:
|
if entry.unique_id == mac or entry.data[CONF_HOST] == host:
|
||||||
if not entry.unique_id:
|
if async_update_entry_from_discovery(self.hass, entry, device):
|
||||||
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")
|
return self.async_abort(reason="already_configured")
|
||||||
self.context[CONF_HOST] = host
|
self.context[CONF_HOST] = host
|
||||||
for progress in self._async_in_progress():
|
for progress in self._async_in_progress():
|
||||||
|
@ -164,12 +166,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
"""Create a config entry from a device."""
|
"""Create a config entry from a device."""
|
||||||
self._async_abort_entries_match({CONF_HOST: device[ATTR_IPADDR]})
|
self._async_abort_entries_match({CONF_HOST: device[ATTR_IPADDR]})
|
||||||
name = async_name_from_discovery(device)
|
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(
|
return self.async_create_entry(
|
||||||
title=name,
|
title=name,
|
||||||
data={
|
data=data,
|
||||||
CONF_HOST: device[ATTR_IPADDR],
|
|
||||||
CONF_NAME: name,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_step_user(
|
async def async_step_user(
|
||||||
|
@ -259,7 +263,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
model=model,
|
model=model,
|
||||||
id=mac_address,
|
id=mac_address,
|
||||||
model_num=bulb.model_num,
|
model_num=bulb.model_num,
|
||||||
version_num=bulb.version_num,
|
version_num=None, # This is the minor version number
|
||||||
firmware_date=None,
|
firmware_date=None,
|
||||||
model_info=None,
|
model_info=None,
|
||||||
model_description=bulb.model_data.description,
|
model_description=bulb.model_data.description,
|
||||||
|
|
|
@ -53,6 +53,10 @@ DISCOVER_SCAN_TIMEOUT: Final = 10
|
||||||
CONF_DEVICES: Final = "devices"
|
CONF_DEVICES: Final = "devices"
|
||||||
CONF_CUSTOM_EFFECT: Final = "custom_effect"
|
CONF_CUSTOM_EFFECT: Final = "custom_effect"
|
||||||
CONF_MODEL: Final = "model"
|
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_AUTO: Final = "auto"
|
||||||
MODE_RGB: Final = "rgb"
|
MODE_RGB: Final = "rgb"
|
||||||
|
|
|
@ -2,21 +2,54 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from collections.abc import Mapping
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Any, Final
|
||||||
|
|
||||||
from flux_led.aioscanner import AIOBulbScanner
|
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 flux_led.scanner import FluxLEDDiscovery
|
||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.components import network
|
from homeassistant.components import network
|
||||||
|
from homeassistant.const import CONF_HOST, CONF_NAME
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers import device_registry as dr
|
||||||
|
from homeassistant.util.network import is_ip_address
|
||||||
|
|
||||||
from .const import DISCOVER_SCAN_TIMEOUT, DOMAIN
|
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__)
|
_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
|
@callback
|
||||||
def async_name_from_discovery(device: FluxLEDDiscovery) -> str:
|
def async_name_from_discovery(device: FluxLEDDiscovery) -> str:
|
||||||
"""Convert a flux_led discovery to a human readable name."""
|
"""Convert a flux_led discovery to a human readable name."""
|
||||||
|
@ -29,6 +62,62 @@ def async_name_from_discovery(device: FluxLEDDiscovery) -> str:
|
||||||
return f"{device[ATTR_MODEL]} {short_mac}"
|
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(
|
async def async_discover_devices(
|
||||||
hass: HomeAssistant, timeout: int, address: str | None = None
|
hass: HomeAssistant, timeout: int, address: str | None = None
|
||||||
) -> list[FluxLEDDiscovery]:
|
) -> list[FluxLEDDiscovery]:
|
||||||
|
|
|
@ -6,18 +6,56 @@ from typing import Any
|
||||||
|
|
||||||
from flux_led.aiodevice import AIOWifiLedBulb
|
from flux_led.aiodevice import AIOWifiLedBulb
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.const import CONF_NAME
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers import device_registry as dr
|
from homeassistant.helpers import device_registry as dr
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.entity import DeviceInfo
|
from homeassistant.helpers.entity import DeviceInfo, Entity
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from . import FluxLedUpdateCoordinator
|
from . import FluxLedUpdateCoordinator
|
||||||
from .const import SIGNAL_STATE_UPDATED
|
from .const import CONF_MINOR_VERSION, CONF_MODEL, SIGNAL_STATE_UPDATED
|
||||||
|
|
||||||
|
|
||||||
|
def _async_device_info(
|
||||||
|
unique_id: str, device: AIOWifiLedBulb, entry: config_entries.ConfigEntry
|
||||||
|
) -> DeviceInfo:
|
||||||
|
version_num = device.version_num
|
||||||
|
if minor_version := entry.data.get(CONF_MINOR_VERSION):
|
||||||
|
sw_version = version_num + int(hex(minor_version)[2:]) / 100
|
||||||
|
sw_version_str = f"{sw_version:0.3f}"
|
||||||
|
else:
|
||||||
|
sw_version_str = str(device.version_num)
|
||||||
|
return DeviceInfo(
|
||||||
|
connections={(dr.CONNECTION_NETWORK_MAC, unique_id)},
|
||||||
|
manufacturer="Zengge",
|
||||||
|
model=device.model,
|
||||||
|
name=entry.data[CONF_NAME],
|
||||||
|
sw_version=sw_version_str,
|
||||||
|
hw_version=entry.data.get(CONF_MODEL),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FluxBaseEntity(Entity):
|
||||||
|
"""Representation of a Flux entity without a coordinator."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
device: AIOWifiLedBulb,
|
||||||
|
entry: config_entries.ConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the light."""
|
||||||
|
self._device: AIOWifiLedBulb = device
|
||||||
|
self.entry = entry
|
||||||
|
if entry.unique_id:
|
||||||
|
self._attr_device_info = _async_device_info(
|
||||||
|
entry.unique_id, self._device, entry
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class FluxEntity(CoordinatorEntity):
|
class FluxEntity(CoordinatorEntity):
|
||||||
"""Representation of a Flux entity."""
|
"""Representation of a Flux entity with a coordinator."""
|
||||||
|
|
||||||
coordinator: FluxLedUpdateCoordinator
|
coordinator: FluxLedUpdateCoordinator
|
||||||
|
|
||||||
|
@ -33,13 +71,9 @@ class FluxEntity(CoordinatorEntity):
|
||||||
self._responding = True
|
self._responding = True
|
||||||
self._attr_name = name
|
self._attr_name = name
|
||||||
self._attr_unique_id = unique_id
|
self._attr_unique_id = unique_id
|
||||||
if self.unique_id:
|
if unique_id:
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = _async_device_info(
|
||||||
connections={(dr.CONNECTION_NETWORK_MAC, self.unique_id)},
|
unique_id, self._device, coordinator.entry
|
||||||
manufacturer="Magic Home (Zengge)",
|
|
||||||
model=self._device.model,
|
|
||||||
name=self.name,
|
|
||||||
sw_version=str(self._device.version_num),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
@ -34,7 +34,6 @@ from homeassistant.const import (
|
||||||
CONF_DEVICES,
|
CONF_DEVICES,
|
||||||
CONF_HOST,
|
CONF_HOST,
|
||||||
CONF_MAC,
|
CONF_MAC,
|
||||||
CONF_MODE,
|
|
||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
CONF_PROTOCOL,
|
CONF_PROTOCOL,
|
||||||
)
|
)
|
||||||
|
@ -158,7 +157,6 @@ async def async_setup_platform(
|
||||||
CONF_MAC: discovered_mac_by_host.get(host),
|
CONF_MAC: discovered_mac_by_host.get(host),
|
||||||
CONF_NAME: device_config[CONF_NAME],
|
CONF_NAME: device_config[CONF_NAME],
|
||||||
CONF_PROTOCOL: device_config.get(CONF_PROTOCOL),
|
CONF_PROTOCOL: device_config.get(CONF_PROTOCOL),
|
||||||
CONF_MODE: device_config.get(ATTR_MODE, MODE_AUTO),
|
|
||||||
CONF_CUSTOM_EFFECT_COLORS: custom_effect_colors,
|
CONF_CUSTOM_EFFECT_COLORS: custom_effect_colors,
|
||||||
CONF_CUSTOM_EFFECT_SPEED_PCT: custom_effects.get(
|
CONF_CUSTOM_EFFECT_SPEED_PCT: custom_effects.get(
|
||||||
CONF_SPEED_PCT, DEFAULT_EFFECT_SPEED
|
CONF_SPEED_PCT, DEFAULT_EFFECT_SPEED
|
||||||
|
|
|
@ -3,16 +3,26 @@ from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from flux_led import DeviceType
|
||||||
|
from flux_led.aio import AIOWifiLedBulb
|
||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.components.switch import SwitchEntity
|
from homeassistant.components.switch import SwitchEntity
|
||||||
from homeassistant.const import CONF_NAME
|
from homeassistant.const import CONF_NAME
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity import EntityCategory
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from . import FluxLedUpdateCoordinator
|
from . import FluxLedUpdateCoordinator
|
||||||
from .const import DOMAIN
|
from .const import (
|
||||||
from .entity import FluxOnOffEntity
|
CONF_REMOTE_ACCESS_ENABLED,
|
||||||
|
CONF_REMOTE_ACCESS_HOST,
|
||||||
|
CONF_REMOTE_ACCESS_PORT,
|
||||||
|
DOMAIN,
|
||||||
|
)
|
||||||
|
from .discovery import async_clear_discovery_cache
|
||||||
|
from .entity import FluxBaseEntity, FluxOnOffEntity
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
|
@ -22,15 +32,22 @@ async def async_setup_entry(
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the Flux lights."""
|
"""Set up the Flux lights."""
|
||||||
coordinator: FluxLedUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
coordinator: FluxLedUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||||
async_add_entities(
|
entities: list[FluxSwitch | FluxRemoteAccessSwitch] = []
|
||||||
[
|
|
||||||
|
if coordinator.device.device_type == DeviceType.Switch:
|
||||||
|
entities.append(
|
||||||
FluxSwitch(
|
FluxSwitch(
|
||||||
coordinator,
|
coordinator,
|
||||||
entry.unique_id,
|
entry.unique_id,
|
||||||
entry.data[CONF_NAME],
|
entry.data[CONF_NAME],
|
||||||
)
|
)
|
||||||
]
|
)
|
||||||
)
|
|
||||||
|
if entry.data.get(CONF_REMOTE_ACCESS_HOST):
|
||||||
|
entities.append(FluxRemoteAccessSwitch(coordinator.device, entry))
|
||||||
|
|
||||||
|
if entities:
|
||||||
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
class FluxSwitch(FluxOnOffEntity, CoordinatorEntity, SwitchEntity):
|
class FluxSwitch(FluxOnOffEntity, CoordinatorEntity, SwitchEntity):
|
||||||
|
@ -40,3 +57,53 @@ class FluxSwitch(FluxOnOffEntity, CoordinatorEntity, SwitchEntity):
|
||||||
"""Turn the device on."""
|
"""Turn the device on."""
|
||||||
if not self.is_on:
|
if not self.is_on:
|
||||||
await self._device.async_turn_on()
|
await self._device.async_turn_on()
|
||||||
|
|
||||||
|
|
||||||
|
class FluxRemoteAccessSwitch(FluxBaseEntity, SwitchEntity):
|
||||||
|
"""Representation of a Flux remote access switch."""
|
||||||
|
|
||||||
|
_attr_should_poll = False
|
||||||
|
_attr_entity_category = EntityCategory.CONFIG
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
device: AIOWifiLedBulb,
|
||||||
|
entry: config_entries.ConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the light."""
|
||||||
|
super().__init__(device, entry)
|
||||||
|
self._attr_name = f"{entry.data[CONF_NAME]} Remote Access"
|
||||||
|
if entry.unique_id:
|
||||||
|
self._attr_unique_id = f"{entry.unique_id}_remote_access"
|
||||||
|
|
||||||
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn the remote access on."""
|
||||||
|
await self._device.async_enable_remote_access(
|
||||||
|
self.entry.data[CONF_REMOTE_ACCESS_HOST],
|
||||||
|
self.entry.data[CONF_REMOTE_ACCESS_PORT],
|
||||||
|
)
|
||||||
|
await self._async_update_entry(True)
|
||||||
|
|
||||||
|
async def _async_update_entry(self, new_state: bool) -> None:
|
||||||
|
"""Update the entry with the new state on success."""
|
||||||
|
async_clear_discovery_cache(self.hass, self._device.ipaddr)
|
||||||
|
self.hass.config_entries.async_update_entry(
|
||||||
|
self.entry,
|
||||||
|
data={**self.entry.data, CONF_REMOTE_ACCESS_ENABLED: new_state},
|
||||||
|
)
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn the remote access off."""
|
||||||
|
await self._device.async_disable_remote_access()
|
||||||
|
await self._async_update_entry(False)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self) -> bool:
|
||||||
|
"""Return true if remote access is enabled."""
|
||||||
|
return bool(self.entry.data[CONF_REMOTE_ACCESS_ENABLED])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self) -> str:
|
||||||
|
"""Return icon based on state."""
|
||||||
|
return "mdi:cloud-outline" if self.is_on else "mdi:cloud-off-outline"
|
||||||
|
|
|
@ -27,8 +27,7 @@
|
||||||
"data": {
|
"data": {
|
||||||
"custom_effect_colors": "Custom Effect: List of 1 to 16 [R,G,B] colors. Example: [255,0,255],[60,128,0]",
|
"custom_effect_colors": "Custom Effect: List of 1 to 16 [R,G,B] colors. Example: [255,0,255],[60,128,0]",
|
||||||
"custom_effect_speed_pct": "Custom Effect: Speed in percents for the effect that switch colors.",
|
"custom_effect_speed_pct": "Custom Effect: Speed in percents for the effect that switch colors.",
|
||||||
"custom_effect_transition": "Custom Effect: Type of transition between the colors.",
|
"custom_effect_transition": "Custom Effect: Type of transition between the colors."
|
||||||
"mode": "The chosen brightness mode."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,6 +57,9 @@ FLUX_DISCOVERY = FluxLEDDiscovery(
|
||||||
firmware_date=datetime.date(2021, 5, 5),
|
firmware_date=datetime.date(2021, 5, 5),
|
||||||
model_info=MODEL,
|
model_info=MODEL,
|
||||||
model_description=MODEL_DESCRIPTION,
|
model_description=MODEL_DESCRIPTION,
|
||||||
|
remote_access_enabled=True,
|
||||||
|
remote_access_host="the.cloud",
|
||||||
|
remote_access_port=8816,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -80,6 +83,9 @@ def _mocked_bulb() -> AIOWifiLedBulb:
|
||||||
bulb.async_turn_off = AsyncMock()
|
bulb.async_turn_off = AsyncMock()
|
||||||
bulb.async_turn_on = AsyncMock()
|
bulb.async_turn_on = AsyncMock()
|
||||||
bulb.async_set_levels = AsyncMock()
|
bulb.async_set_levels = AsyncMock()
|
||||||
|
bulb.async_set_zones = AsyncMock()
|
||||||
|
bulb.async_disable_remote_access = AsyncMock()
|
||||||
|
bulb.async_enable_remote_access = AsyncMock()
|
||||||
bulb.min_temp = 2700
|
bulb.min_temp = 2700
|
||||||
bulb.max_temp = 6500
|
bulb.max_temp = 6500
|
||||||
bulb.getRgb = MagicMock(return_value=[255, 0, 0])
|
bulb.getRgb = MagicMock(return_value=[255, 0, 0])
|
||||||
|
|
|
@ -11,6 +11,11 @@ from homeassistant.components.flux_led.const import (
|
||||||
CONF_CUSTOM_EFFECT_COLORS,
|
CONF_CUSTOM_EFFECT_COLORS,
|
||||||
CONF_CUSTOM_EFFECT_SPEED_PCT,
|
CONF_CUSTOM_EFFECT_SPEED_PCT,
|
||||||
CONF_CUSTOM_EFFECT_TRANSITION,
|
CONF_CUSTOM_EFFECT_TRANSITION,
|
||||||
|
CONF_MINOR_VERSION,
|
||||||
|
CONF_MODEL,
|
||||||
|
CONF_REMOTE_ACCESS_ENABLED,
|
||||||
|
CONF_REMOTE_ACCESS_HOST,
|
||||||
|
CONF_REMOTE_ACCESS_PORT,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
MODE_RGB,
|
MODE_RGB,
|
||||||
TRANSITION_JUMP,
|
TRANSITION_JUMP,
|
||||||
|
@ -20,7 +25,6 @@ from homeassistant.const import (
|
||||||
CONF_DEVICE,
|
CONF_DEVICE,
|
||||||
CONF_HOST,
|
CONF_HOST,
|
||||||
CONF_MAC,
|
CONF_MAC,
|
||||||
CONF_MODE,
|
|
||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
CONF_PROTOCOL,
|
CONF_PROTOCOL,
|
||||||
)
|
)
|
||||||
|
@ -34,6 +38,7 @@ from . import (
|
||||||
FLUX_DISCOVERY_PARTIAL,
|
FLUX_DISCOVERY_PARTIAL,
|
||||||
IP_ADDRESS,
|
IP_ADDRESS,
|
||||||
MAC_ADDRESS,
|
MAC_ADDRESS,
|
||||||
|
MODEL,
|
||||||
MODULE,
|
MODULE,
|
||||||
_patch_discovery,
|
_patch_discovery,
|
||||||
_patch_wifibulb,
|
_patch_wifibulb,
|
||||||
|
@ -88,7 +93,16 @@ async def test_discovery(hass: HomeAssistant):
|
||||||
|
|
||||||
assert result3["type"] == "create_entry"
|
assert result3["type"] == "create_entry"
|
||||||
assert result3["title"] == DEFAULT_ENTRY_TITLE
|
assert result3["title"] == DEFAULT_ENTRY_TITLE
|
||||||
assert result3["data"] == {CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}
|
assert result3["data"] == {
|
||||||
|
CONF_MINOR_VERSION: 4,
|
||||||
|
CONF_HOST: IP_ADDRESS,
|
||||||
|
CONF_NAME: DEFAULT_ENTRY_TITLE,
|
||||||
|
CONF_MODEL: MODEL,
|
||||||
|
CONF_REMOTE_ACCESS_ENABLED: True,
|
||||||
|
CONF_REMOTE_ACCESS_HOST: "the.cloud",
|
||||||
|
CONF_REMOTE_ACCESS_PORT: 8816,
|
||||||
|
CONF_MINOR_VERSION: 0x04,
|
||||||
|
}
|
||||||
mock_setup.assert_called_once()
|
mock_setup.assert_called_once()
|
||||||
mock_setup_entry.assert_called_once()
|
mock_setup_entry.assert_called_once()
|
||||||
|
|
||||||
|
@ -160,8 +174,14 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant):
|
||||||
assert result3["type"] == "create_entry"
|
assert result3["type"] == "create_entry"
|
||||||
assert result3["title"] == DEFAULT_ENTRY_TITLE
|
assert result3["title"] == DEFAULT_ENTRY_TITLE
|
||||||
assert result3["data"] == {
|
assert result3["data"] == {
|
||||||
|
CONF_MINOR_VERSION: 4,
|
||||||
CONF_HOST: IP_ADDRESS,
|
CONF_HOST: IP_ADDRESS,
|
||||||
CONF_NAME: DEFAULT_ENTRY_TITLE,
|
CONF_NAME: DEFAULT_ENTRY_TITLE,
|
||||||
|
CONF_MODEL: MODEL,
|
||||||
|
CONF_REMOTE_ACCESS_ENABLED: True,
|
||||||
|
CONF_REMOTE_ACCESS_HOST: "the.cloud",
|
||||||
|
CONF_REMOTE_ACCESS_PORT: 8816,
|
||||||
|
CONF_MINOR_VERSION: 0x04,
|
||||||
}
|
}
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
@ -204,7 +224,7 @@ async def test_import(hass: HomeAssistant):
|
||||||
CONF_MAC: MAC_ADDRESS,
|
CONF_MAC: MAC_ADDRESS,
|
||||||
CONF_NAME: "floor lamp",
|
CONF_NAME: "floor lamp",
|
||||||
CONF_PROTOCOL: "ledenet",
|
CONF_PROTOCOL: "ledenet",
|
||||||
CONF_MODE: MODE_RGB,
|
CONF_MODEL: MODE_RGB,
|
||||||
CONF_CUSTOM_EFFECT_COLORS: "[255,0,0], [0,0,255]",
|
CONF_CUSTOM_EFFECT_COLORS: "[255,0,0], [0,0,255]",
|
||||||
CONF_CUSTOM_EFFECT_SPEED_PCT: 30,
|
CONF_CUSTOM_EFFECT_SPEED_PCT: 30,
|
||||||
CONF_CUSTOM_EFFECT_TRANSITION: TRANSITION_STROBE,
|
CONF_CUSTOM_EFFECT_TRANSITION: TRANSITION_STROBE,
|
||||||
|
@ -229,7 +249,6 @@ async def test_import(hass: HomeAssistant):
|
||||||
CONF_PROTOCOL: "ledenet",
|
CONF_PROTOCOL: "ledenet",
|
||||||
}
|
}
|
||||||
assert result["options"] == {
|
assert result["options"] == {
|
||||||
CONF_MODE: MODE_RGB,
|
|
||||||
CONF_CUSTOM_EFFECT_COLORS: "[255,0,0], [0,0,255]",
|
CONF_CUSTOM_EFFECT_COLORS: "[255,0,0], [0,0,255]",
|
||||||
CONF_CUSTOM_EFFECT_SPEED_PCT: 30,
|
CONF_CUSTOM_EFFECT_SPEED_PCT: 30,
|
||||||
CONF_CUSTOM_EFFECT_TRANSITION: TRANSITION_STROBE,
|
CONF_CUSTOM_EFFECT_TRANSITION: TRANSITION_STROBE,
|
||||||
|
@ -278,7 +297,16 @@ async def test_manual_working_discovery(hass: HomeAssistant):
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert result4["type"] == "create_entry"
|
assert result4["type"] == "create_entry"
|
||||||
assert result4["title"] == DEFAULT_ENTRY_TITLE
|
assert result4["title"] == DEFAULT_ENTRY_TITLE
|
||||||
assert result4["data"] == {CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}
|
assert result4["data"] == {
|
||||||
|
CONF_MINOR_VERSION: 4,
|
||||||
|
CONF_HOST: IP_ADDRESS,
|
||||||
|
CONF_NAME: DEFAULT_ENTRY_TITLE,
|
||||||
|
CONF_MODEL: MODEL,
|
||||||
|
CONF_REMOTE_ACCESS_ENABLED: True,
|
||||||
|
CONF_REMOTE_ACCESS_HOST: "the.cloud",
|
||||||
|
CONF_REMOTE_ACCESS_PORT: 8816,
|
||||||
|
CONF_MINOR_VERSION: 0x04,
|
||||||
|
}
|
||||||
|
|
||||||
# Duplicate
|
# Duplicate
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
@ -376,7 +404,16 @@ async def test_discovered_by_discovery(hass):
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert result2["type"] == "create_entry"
|
assert result2["type"] == "create_entry"
|
||||||
assert result2["data"] == {CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}
|
assert result2["data"] == {
|
||||||
|
CONF_MINOR_VERSION: 4,
|
||||||
|
CONF_HOST: IP_ADDRESS,
|
||||||
|
CONF_NAME: DEFAULT_ENTRY_TITLE,
|
||||||
|
CONF_MODEL: MODEL,
|
||||||
|
CONF_REMOTE_ACCESS_ENABLED: True,
|
||||||
|
CONF_REMOTE_ACCESS_HOST: "the.cloud",
|
||||||
|
CONF_REMOTE_ACCESS_PORT: 8816,
|
||||||
|
CONF_MINOR_VERSION: 0x04,
|
||||||
|
}
|
||||||
assert mock_async_setup.called
|
assert mock_async_setup.called
|
||||||
assert mock_async_setup_entry.called
|
assert mock_async_setup_entry.called
|
||||||
|
|
||||||
|
@ -402,7 +439,16 @@ async def test_discovered_by_dhcp_udp_responds(hass):
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert result2["type"] == "create_entry"
|
assert result2["type"] == "create_entry"
|
||||||
assert result2["data"] == {CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}
|
assert result2["data"] == {
|
||||||
|
CONF_MINOR_VERSION: 4,
|
||||||
|
CONF_HOST: IP_ADDRESS,
|
||||||
|
CONF_NAME: DEFAULT_ENTRY_TITLE,
|
||||||
|
CONF_MODEL: MODEL,
|
||||||
|
CONF_REMOTE_ACCESS_ENABLED: True,
|
||||||
|
CONF_REMOTE_ACCESS_HOST: "the.cloud",
|
||||||
|
CONF_REMOTE_ACCESS_PORT: 8816,
|
||||||
|
CONF_MINOR_VERSION: 0x04,
|
||||||
|
}
|
||||||
assert mock_async_setup.called
|
assert mock_async_setup.called
|
||||||
assert mock_async_setup_entry.called
|
assert mock_async_setup_entry.called
|
||||||
|
|
||||||
|
@ -538,7 +584,7 @@ async def test_options(hass: HomeAssistant):
|
||||||
domain=DOMAIN,
|
domain=DOMAIN,
|
||||||
data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE},
|
data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE},
|
||||||
options={
|
options={
|
||||||
CONF_MODE: MODE_RGB,
|
CONF_MODEL: MODE_RGB,
|
||||||
CONF_CUSTOM_EFFECT_COLORS: "[255,0,0], [0,0,255]",
|
CONF_CUSTOM_EFFECT_COLORS: "[255,0,0], [0,0,255]",
|
||||||
CONF_CUSTOM_EFFECT_SPEED_PCT: 30,
|
CONF_CUSTOM_EFFECT_SPEED_PCT: 30,
|
||||||
CONF_CUSTOM_EFFECT_TRANSITION: TRANSITION_STROBE,
|
CONF_CUSTOM_EFFECT_TRANSITION: TRANSITION_STROBE,
|
||||||
|
|
|
@ -3,8 +3,6 @@ from __future__ import annotations
|
||||||
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from flux_led.aioscanner import AIOBulbScanner
|
|
||||||
from flux_led.scanner import FluxLEDDiscovery
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components import flux_led
|
from homeassistant.components import flux_led
|
||||||
|
@ -107,32 +105,24 @@ async def test_config_entry_retry(hass: HomeAssistant) -> None:
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_config_entry_fills_unique_id_with_directed_discovery(
|
async def test_config_entry_fills_unique_id_with_directed_discovery(
|
||||||
hass: HomeAssistant, discovery: FluxLEDDiscovery, title: str
|
hass: HomeAssistant, discovery: dict[str, str], title: str
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test that the unique id is added if its missing via directed (not broadcast) discovery."""
|
"""Test that the unique id is added if its missing via directed (not broadcast) discovery."""
|
||||||
config_entry = MockConfigEntry(
|
config_entry = MockConfigEntry(
|
||||||
domain=DOMAIN, data={CONF_NAME: "bogus", CONF_HOST: IP_ADDRESS}, unique_id=None
|
domain=DOMAIN, data={CONF_HOST: IP_ADDRESS}, unique_id=None
|
||||||
)
|
)
|
||||||
config_entry.add_to_hass(hass)
|
config_entry.add_to_hass(hass)
|
||||||
assert config_entry.unique_id is None
|
|
||||||
|
|
||||||
class MockBulbScanner(AIOBulbScanner):
|
async def _discovery(self, *args, address=None, **kwargs):
|
||||||
def __init__(self) -> None:
|
# Only return discovery results when doing directed discovery
|
||||||
self._last_address: str | None = None
|
return [discovery] if address == IP_ADDRESS else []
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
async def async_scan(
|
|
||||||
self, timeout: int = 10, address: str | None = None
|
|
||||||
) -> list[FluxLEDDiscovery]:
|
|
||||||
self._last_address = address
|
|
||||||
return [discovery] if address == IP_ADDRESS else []
|
|
||||||
|
|
||||||
def getBulbInfo(self) -> FluxLEDDiscovery:
|
|
||||||
return [discovery] if self._last_address == IP_ADDRESS else []
|
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.flux_led.discovery.AIOBulbScanner",
|
"homeassistant.components.flux_led.discovery.AIOBulbScanner.async_scan",
|
||||||
return_value=MockBulbScanner(),
|
new=_discovery,
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.flux_led.discovery.AIOBulbScanner.getBulbInfo",
|
||||||
|
return_value=[discovery],
|
||||||
), _patch_wifibulb():
|
), _patch_wifibulb():
|
||||||
await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}})
|
await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}})
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
"""Tests for light platform."""
|
"""Tests for light platform."""
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from unittest.mock import AsyncMock, Mock
|
from unittest.mock import AsyncMock, Mock, patch
|
||||||
|
|
||||||
from flux_led.const import (
|
from flux_led.const import (
|
||||||
COLOR_MODE_ADDRESSABLE as FLUX_COLOR_MODE_ADDRESSABLE,
|
COLOR_MODE_ADDRESSABLE as FLUX_COLOR_MODE_ADDRESSABLE,
|
||||||
|
@ -21,6 +21,11 @@ from homeassistant.components.flux_led.const import (
|
||||||
CONF_CUSTOM_EFFECT_SPEED_PCT,
|
CONF_CUSTOM_EFFECT_SPEED_PCT,
|
||||||
CONF_CUSTOM_EFFECT_TRANSITION,
|
CONF_CUSTOM_EFFECT_TRANSITION,
|
||||||
CONF_DEVICES,
|
CONF_DEVICES,
|
||||||
|
CONF_MINOR_VERSION,
|
||||||
|
CONF_MODEL,
|
||||||
|
CONF_REMOTE_ACCESS_ENABLED,
|
||||||
|
CONF_REMOTE_ACCESS_HOST,
|
||||||
|
CONF_REMOTE_ACCESS_PORT,
|
||||||
CONF_SPEED_PCT,
|
CONF_SPEED_PCT,
|
||||||
CONF_TRANSITION,
|
CONF_TRANSITION,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
@ -59,8 +64,10 @@ from homeassistant.util.dt import utcnow
|
||||||
|
|
||||||
from . import (
|
from . import (
|
||||||
DEFAULT_ENTRY_TITLE,
|
DEFAULT_ENTRY_TITLE,
|
||||||
|
FLUX_DISCOVERY,
|
||||||
IP_ADDRESS,
|
IP_ADDRESS,
|
||||||
MAC_ADDRESS,
|
MAC_ADDRESS,
|
||||||
|
MODEL,
|
||||||
_mocked_bulb,
|
_mocked_bulb,
|
||||||
_patch_discovery,
|
_patch_discovery,
|
||||||
_patch_wifibulb,
|
_patch_wifibulb,
|
||||||
|
@ -1145,7 +1152,26 @@ async def test_migrate_from_yaml_with_custom_effect(hass: HomeAssistant) -> None
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
with _patch_discovery(), _patch_wifibulb():
|
|
||||||
|
last_address = None
|
||||||
|
|
||||||
|
async def _discovery(self, *args, address=None, **kwargs):
|
||||||
|
# Only return discovery results when doing directed discovery
|
||||||
|
nonlocal last_address
|
||||||
|
last_address = address
|
||||||
|
return [FLUX_DISCOVERY] if address == IP_ADDRESS else []
|
||||||
|
|
||||||
|
def _mock_getBulbInfo(*args, **kwargs):
|
||||||
|
nonlocal last_address
|
||||||
|
return [FLUX_DISCOVERY] if last_address == IP_ADDRESS else []
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.flux_led.discovery.AIOBulbScanner.async_scan",
|
||||||
|
new=_discovery,
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.flux_led.discovery.AIOBulbScanner.getBulbInfo",
|
||||||
|
new=_mock_getBulbInfo,
|
||||||
|
), _patch_wifibulb():
|
||||||
await async_setup_component(hass, LIGHT_DOMAIN, config)
|
await async_setup_component(hass, LIGHT_DOMAIN, config)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
@ -1165,9 +1191,13 @@ async def test_migrate_from_yaml_with_custom_effect(hass: HomeAssistant) -> None
|
||||||
CONF_HOST: IP_ADDRESS,
|
CONF_HOST: IP_ADDRESS,
|
||||||
CONF_NAME: "flux_lamppost",
|
CONF_NAME: "flux_lamppost",
|
||||||
CONF_PROTOCOL: "ledenet",
|
CONF_PROTOCOL: "ledenet",
|
||||||
|
CONF_MODEL: MODEL,
|
||||||
|
CONF_REMOTE_ACCESS_ENABLED: True,
|
||||||
|
CONF_REMOTE_ACCESS_HOST: "the.cloud",
|
||||||
|
CONF_REMOTE_ACCESS_PORT: 8816,
|
||||||
|
CONF_MINOR_VERSION: 0x04,
|
||||||
}
|
}
|
||||||
assert migrated_entry.options == {
|
assert migrated_entry.options == {
|
||||||
CONF_MODE: "auto",
|
|
||||||
CONF_CUSTOM_EFFECT_COLORS: "[(255, 0, 0), (255, 255, 0), (0, 255, 0)]",
|
CONF_CUSTOM_EFFECT_COLORS: "[(255, 0, 0), (255, 255, 0), (0, 255, 0)]",
|
||||||
CONF_CUSTOM_EFFECT_SPEED_PCT: 30,
|
CONF_CUSTOM_EFFECT_SPEED_PCT: 30,
|
||||||
CONF_CUSTOM_EFFECT_TRANSITION: "strobe",
|
CONF_CUSTOM_EFFECT_TRANSITION: "strobe",
|
||||||
|
@ -1189,7 +1219,26 @@ async def test_migrate_from_yaml_no_custom_effect(hass: HomeAssistant) -> None:
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
with _patch_discovery(), _patch_wifibulb():
|
|
||||||
|
last_address = None
|
||||||
|
|
||||||
|
async def _discovery(self, *args, address=None, **kwargs):
|
||||||
|
# Only return discovery results when doing directed discovery
|
||||||
|
nonlocal last_address
|
||||||
|
last_address = address
|
||||||
|
return [FLUX_DISCOVERY] if address == IP_ADDRESS else []
|
||||||
|
|
||||||
|
def _mock_getBulbInfo(*args, **kwargs):
|
||||||
|
nonlocal last_address
|
||||||
|
return [FLUX_DISCOVERY] if last_address == IP_ADDRESS else []
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.flux_led.discovery.AIOBulbScanner.async_scan",
|
||||||
|
new=_discovery,
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.flux_led.discovery.AIOBulbScanner.getBulbInfo",
|
||||||
|
new=_mock_getBulbInfo,
|
||||||
|
), _patch_wifibulb():
|
||||||
await async_setup_component(hass, LIGHT_DOMAIN, config)
|
await async_setup_component(hass, LIGHT_DOMAIN, config)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
@ -1209,9 +1258,13 @@ async def test_migrate_from_yaml_no_custom_effect(hass: HomeAssistant) -> None:
|
||||||
CONF_HOST: IP_ADDRESS,
|
CONF_HOST: IP_ADDRESS,
|
||||||
CONF_NAME: "flux_lamppost",
|
CONF_NAME: "flux_lamppost",
|
||||||
CONF_PROTOCOL: "ledenet",
|
CONF_PROTOCOL: "ledenet",
|
||||||
|
CONF_MODEL: MODEL,
|
||||||
|
CONF_REMOTE_ACCESS_ENABLED: True,
|
||||||
|
CONF_REMOTE_ACCESS_HOST: "the.cloud",
|
||||||
|
CONF_REMOTE_ACCESS_PORT: 8816,
|
||||||
|
CONF_MINOR_VERSION: 0x04,
|
||||||
}
|
}
|
||||||
assert migrated_entry.options == {
|
assert migrated_entry.options == {
|
||||||
CONF_MODE: "auto",
|
|
||||||
CONF_CUSTOM_EFFECT_COLORS: None,
|
CONF_CUSTOM_EFFECT_COLORS: None,
|
||||||
CONF_CUSTOM_EFFECT_SPEED_PCT: 50,
|
CONF_CUSTOM_EFFECT_SPEED_PCT: 50,
|
||||||
CONF_CUSTOM_EFFECT_TRANSITION: "gradual",
|
CONF_CUSTOM_EFFECT_TRANSITION: "gradual",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
"""Tests for switch platform."""
|
"""Tests for switch platform."""
|
||||||
from homeassistant.components import flux_led
|
from homeassistant.components import flux_led
|
||||||
from homeassistant.components.flux_led.const import DOMAIN
|
from homeassistant.components.flux_led.const import CONF_REMOTE_ACCESS_ENABLED, DOMAIN
|
||||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ENTITY_ID,
|
ATTR_ENTITY_ID,
|
||||||
|
@ -16,6 +16,7 @@ from . import (
|
||||||
DEFAULT_ENTRY_TITLE,
|
DEFAULT_ENTRY_TITLE,
|
||||||
IP_ADDRESS,
|
IP_ADDRESS,
|
||||||
MAC_ADDRESS,
|
MAC_ADDRESS,
|
||||||
|
_mocked_bulb,
|
||||||
_mocked_switch,
|
_mocked_switch,
|
||||||
_patch_discovery,
|
_patch_discovery,
|
||||||
_patch_wifibulb,
|
_patch_wifibulb,
|
||||||
|
@ -27,7 +28,7 @@ from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
async def test_switch_on_off(hass: HomeAssistant) -> None:
|
async def test_switch_on_off(hass: HomeAssistant) -> None:
|
||||||
"""Test a switch light."""
|
"""Test a smart plug."""
|
||||||
config_entry = MockConfigEntry(
|
config_entry = MockConfigEntry(
|
||||||
domain=DOMAIN,
|
domain=DOMAIN,
|
||||||
data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE},
|
data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE},
|
||||||
|
@ -60,3 +61,37 @@ async def test_switch_on_off(hass: HomeAssistant) -> None:
|
||||||
|
|
||||||
await async_mock_device_turn_on(hass, switch)
|
await async_mock_device_turn_on(hass, switch)
|
||||||
assert hass.states.get(entity_id).state == STATE_ON
|
assert hass.states.get(entity_id).state == STATE_ON
|
||||||
|
|
||||||
|
|
||||||
|
async def test_remote_access_on_off(hass: HomeAssistant) -> None:
|
||||||
|
"""Test enable/disable remote access."""
|
||||||
|
config_entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE},
|
||||||
|
unique_id=MAC_ADDRESS,
|
||||||
|
)
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
bulb = _mocked_bulb()
|
||||||
|
with _patch_discovery(), _patch_wifibulb(bulb):
|
||||||
|
await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
entity_id = "switch.bulb_rgbcw_ddeeff_remote_access"
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state.state == STATE_ON
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
SWITCH_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||||
|
)
|
||||||
|
bulb.async_disable_remote_access.assert_called_once()
|
||||||
|
assert hass.states.get(entity_id).state == STATE_OFF
|
||||||
|
assert config_entry.data[CONF_REMOTE_ACCESS_ENABLED] is False
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
SWITCH_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||||
|
)
|
||||||
|
bulb.async_enable_remote_access.assert_called_once()
|
||||||
|
|
||||||
|
assert hass.states.get(entity_id).state == STATE_ON
|
||||||
|
assert config_entry.data[CONF_REMOTE_ACCESS_ENABLED] is True
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue