Refactor LIFX discovery to make it faster and more reliable (#70458)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Avi Miller 2022-04-27 04:58:01 +10:00 committed by GitHub
parent c15c22655b
commit f593b387a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 33 additions and 22 deletions

View File

@ -4,6 +4,7 @@ from __future__ import annotations
import asyncio import asyncio
from datetime import timedelta from datetime import timedelta
from functools import partial from functools import partial
from ipaddress import IPv4Address
import logging import logging
import math import math
@ -13,6 +14,7 @@ from awesomeversion import AwesomeVersion
import voluptuous as vol import voluptuous as vol
from homeassistant import util from homeassistant import util
from homeassistant.components import network
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_BRIGHTNESS,
ATTR_BRIGHTNESS_PCT, ATTR_BRIGHTNESS_PCT,
@ -69,8 +71,8 @@ _LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=10) SCAN_INTERVAL = timedelta(seconds=10)
DISCOVERY_INTERVAL = 60 DISCOVERY_INTERVAL = 60
MESSAGE_TIMEOUT = 1.0 MESSAGE_TIMEOUT = 1
MESSAGE_RETRIES = 8 MESSAGE_RETRIES = 3
UNAVAILABLE_GRACE = 90 UNAVAILABLE_GRACE = 90
FIX_MAC_FW = AwesomeVersion("3.70") FIX_MAC_FW = AwesomeVersion("3.70")
@ -193,12 +195,13 @@ async def async_setup_entry(
"""Set up LIFX from a config entry.""" """Set up LIFX from a config entry."""
# Priority 1: manual config # Priority 1: manual config
if not (interfaces := hass.data[LIFX_DOMAIN].get(DOMAIN)): if not (interfaces := hass.data[LIFX_DOMAIN].get(DOMAIN)):
# Priority 2: scanned interfaces # Priority 2: Home Assistant enabled interfaces
lifx_ip_addresses = await aiolifx().LifxScan(hass.loop).scan() ip_addresses = (
interfaces = [{CONF_SERVER: ip} for ip in lifx_ip_addresses] source_ip
if not interfaces: for source_ip in await network.async_get_enabled_source_ips(hass)
# Priority 3: default interface if isinstance(source_ip, IPv4Address) and not source_ip.is_loopback
interfaces = [{}] )
interfaces = [{CONF_SERVER: str(ip)} for ip in ip_addresses]
platform = entity_platform.async_get_current_platform() platform = entity_platform.async_get_current_platform()
lifx_manager = LIFXManager(hass, platform, config_entry, async_add_entities) lifx_manager = LIFXManager(hass, platform, config_entry, async_add_entities)
@ -259,6 +262,7 @@ class LIFXManager:
def __init__(self, hass, platform, config_entry, async_add_entities): def __init__(self, hass, platform, config_entry, async_add_entities):
"""Initialize the light.""" """Initialize the light."""
self.entities = {} self.entities = {}
self.discoveries_inflight = {}
self.hass = hass self.hass = hass
self.platform = platform self.platform = platform
self.config_entry = config_entry self.config_entry = config_entry
@ -378,18 +382,23 @@ class LIFXManager:
@callback @callback
def register(self, bulb): def register(self, bulb):
"""Handle aiolifx detected bulb.""" """Allow a single in-flight discovery per bulb."""
self.hass.async_create_task(self.register_new_bulb(bulb)) if bulb.mac_addr not in self.discoveries_inflight:
self.discoveries_inflight[bulb.mac_addr] = bulb.ip_addr
_LOGGER.debug("Discovered %s (%s)", bulb.ip_addr, bulb.mac_addr)
self.hass.async_create_task(self.register_bulb(bulb))
else:
_LOGGER.warning("Duplicate LIFX discovery response ignored")
async def register_new_bulb(self, bulb): async def register_bulb(self, bulb):
"""Handle newly detected bulb.""" """Handle LIFX bulb registration lifecycle."""
if bulb.mac_addr in self.entities: if bulb.mac_addr in self.entities:
entity = self.entities[bulb.mac_addr] entity = self.entities[bulb.mac_addr]
entity.registered = True entity.registered = True
_LOGGER.debug("%s register AGAIN", entity.who) _LOGGER.debug("Reconnected to %s", entity.who)
await entity.update_hass() await entity.update_hass()
else: else:
_LOGGER.debug("%s register NEW", bulb.ip_addr) _LOGGER.debug("Connecting to %s (%s)", bulb.ip_addr, bulb.mac_addr)
# Read initial state # Read initial state
ack = AwaitAioLIFX().wait ack = AwaitAioLIFX().wait
@ -398,8 +407,8 @@ class LIFXManager:
# can be ignored. # can be ignored.
version_resp = await ack(bulb.get_version) version_resp = await ack(bulb.get_version)
if version_resp and bulb.product in SWITCH_PRODUCT_IDS: if version_resp and bulb.product in SWITCH_PRODUCT_IDS:
_LOGGER.warning( _LOGGER.debug(
"(Switch) action=skip_discovery, reason=unsupported, serial=%s, ip_addr=%s, type='LIFX Switch'", "Not connecting to LIFX Switch %s (%s)",
str(bulb.mac_addr).replace(":", ""), str(bulb.mac_addr).replace(":", ""),
bulb.ip_addr, bulb.ip_addr,
) )
@ -408,7 +417,7 @@ class LIFXManager:
color_resp = await ack(bulb.get_color) color_resp = await ack(bulb.get_color)
if color_resp is None or version_resp is None: if color_resp is None or version_resp is None:
_LOGGER.error("Failed to initialize %s", bulb.ip_addr) _LOGGER.error("Failed to connect to %s", bulb.ip_addr)
bulb.registered = False bulb.registered = False
else: else:
bulb.timeout = MESSAGE_TIMEOUT bulb.timeout = MESSAGE_TIMEOUT
@ -422,16 +431,17 @@ class LIFXManager:
else: else:
entity = LIFXWhite(bulb, self.effects_conductor) entity = LIFXWhite(bulb, self.effects_conductor)
_LOGGER.debug("%s register READY", entity.who) _LOGGER.debug("Connected to %s", entity.who)
self.entities[bulb.mac_addr] = entity self.entities[bulb.mac_addr] = entity
self.discoveries_inflight.pop(bulb.mac_addr, None)
self.async_add_entities([entity], True) self.async_add_entities([entity], True)
@callback @callback
def unregister(self, bulb): def unregister(self, bulb):
"""Handle aiolifx disappearing bulbs.""" """Disconnect and unregister non-responsive bulbs."""
if bulb.mac_addr in self.entities: if bulb.mac_addr in self.entities:
entity = self.entities[bulb.mac_addr] entity = self.entities[bulb.mac_addr]
_LOGGER.debug("%s unregister", entity.who) _LOGGER.debug("Disconnected from %s", entity.who)
entity.registered = False entity.registered = False
entity.async_write_ha_state() entity.async_write_ha_state()
@ -551,8 +561,8 @@ class LIFXLight(LightEntity):
@property @property
def who(self): def who(self):
"""Return a string identifying the bulb.""" """Return a string identifying the bulb by name and mac."""
return f"{self.bulb.ip_addr} ({self.name})" return f"{self.name} ({self.bulb.mac_addr})"
@property @property
def min_mireds(self): def min_mireds(self):

View File

@ -4,6 +4,7 @@
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/lifx", "documentation": "https://www.home-assistant.io/integrations/lifx",
"requirements": ["aiolifx==0.7.1", "aiolifx_effects==0.2.2"], "requirements": ["aiolifx==0.7.1", "aiolifx_effects==0.2.2"],
"dependencies": ["network"],
"homekit": { "homekit": {
"models": [ "models": [
"LIFX A19", "LIFX A19",