Hue allow per-device availability override (#63025)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>pull/63119/head
parent
ebe9853e6f
commit
055fb99938
|
@ -17,13 +17,15 @@ from homeassistant.components import ssdp, zeroconf
|
|||
from homeassistant.const import CONF_API_KEY, CONF_HOST
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
from homeassistant.helpers import aiohttp_client, device_registry
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import (
|
||||
CONF_ALLOW_HUE_GROUPS,
|
||||
CONF_ALLOW_UNREACHABLE,
|
||||
CONF_API_VERSION,
|
||||
CONF_IGNORE_AVAILABILITY,
|
||||
DEFAULT_ALLOW_HUE_GROUPS,
|
||||
DEFAULT_ALLOW_UNREACHABLE,
|
||||
DOMAIN,
|
||||
|
@ -46,17 +48,11 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
@callback
|
||||
def async_get_options_flow(
|
||||
config_entry: config_entries.ConfigEntry,
|
||||
) -> HueOptionsFlowHandler:
|
||||
) -> HueV1OptionsFlowHandler | HueV2OptionsFlowHandler:
|
||||
"""Get the options flow for this handler."""
|
||||
return HueOptionsFlowHandler(config_entry)
|
||||
|
||||
@classmethod
|
||||
@callback
|
||||
def async_supports_options_flow(
|
||||
cls, config_entry: config_entries.ConfigEntry
|
||||
) -> bool:
|
||||
"""Return options flow support for this handler."""
|
||||
return config_entry.data.get(CONF_API_VERSION, 1) == 1
|
||||
if config_entry.data.get(CONF_API_VERSION, 1) == 1:
|
||||
return HueV1OptionsFlowHandler(config_entry)
|
||||
return HueV2OptionsFlowHandler(config_entry)
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the Hue flow."""
|
||||
|
@ -288,8 +284,8 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
return await self.async_step_link()
|
||||
|
||||
|
||||
class HueOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
"""Handle Hue options."""
|
||||
class HueV1OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
"""Handle Hue options for V1 implementation."""
|
||||
|
||||
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
|
||||
"""Initialize Hue options flow."""
|
||||
|
@ -319,3 +315,47 @@ class HueOptionsFlowHandler(config_entries.OptionsFlow):
|
|||
}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class HueV2OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
"""Handle Hue options for V2 implementation."""
|
||||
|
||||
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
|
||||
"""Initialize Hue options flow."""
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(self, user_input: ConfigType | None = None) -> FlowResult:
|
||||
"""Manage Hue options."""
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
|
||||
# create a list of Hue device ID's that the user can select
|
||||
# to ignore availability status
|
||||
dev_reg = device_registry.async_get(self.hass)
|
||||
entries = device_registry.async_entries_for_config_entry(
|
||||
dev_reg, self.config_entry.entry_id
|
||||
)
|
||||
dev_ids = {
|
||||
identifier[1]: entry.name
|
||||
for entry in entries
|
||||
for identifier in entry.identifiers
|
||||
if identifier[0] == DOMAIN
|
||||
}
|
||||
# filter any non existing device id's from the list
|
||||
cur_ids = [
|
||||
item
|
||||
for item in self.config_entry.options.get(CONF_IGNORE_AVAILABILITY, [])
|
||||
if item in dev_ids
|
||||
]
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_IGNORE_AVAILABILITY,
|
||||
default=cur_ids,
|
||||
): cv.multi_select(dev_ids),
|
||||
}
|
||||
),
|
||||
)
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
DOMAIN = "hue"
|
||||
|
||||
CONF_API_VERSION = "api_version"
|
||||
CONF_IGNORE_AVAILABILITY = "ignore_availability"
|
||||
|
||||
CONF_SUBTYPE = "subtype"
|
||||
|
||||
|
|
|
@ -70,7 +70,8 @@
|
|||
"data": {
|
||||
"allow_hue_groups": "Allow Hue groups",
|
||||
"allow_hue_scenes": "Allow Hue scenes",
|
||||
"allow_unreachable": "Allow unreachable bulbs to report their state correctly"
|
||||
"allow_unreachable": "Allow unreachable bulbs to report their state correctly",
|
||||
"ignore_availability": "Ignore connectivity status for the given devices"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -69,7 +69,8 @@
|
|||
"data": {
|
||||
"allow_hue_groups": "Allow Hue groups",
|
||||
"allow_hue_scenes": "Allow Hue scenes",
|
||||
"allow_unreachable": "Allow unreachable bulbs to report their state correctly"
|
||||
"allow_unreachable": "Allow unreachable bulbs to report their state correctly",
|
||||
"ignore_availability": "Ignore connectivity status for the given devices"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -69,7 +69,8 @@
|
|||
"data": {
|
||||
"allow_hue_groups": "Sta Hue-groepen toe",
|
||||
"allow_hue_scenes": "Sta Hue sc\u00e8nes toe",
|
||||
"allow_unreachable": "Onbereikbare lampen toestaan hun status correct te melden"
|
||||
"allow_unreachable": "Onbereikbare lampen toestaan hun status correct te melden",
|
||||
"ignore_availability": "Negeer beschikbaarheid status voor deze apparaten"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ from homeassistant.helpers.entity import DeviceInfo, Entity
|
|||
from homeassistant.helpers.entity_registry import async_get as async_get_entity_registry
|
||||
|
||||
from ..bridge import HueBridge
|
||||
from ..const import DOMAIN
|
||||
from ..const import CONF_IGNORE_AVAILABILITY, DOMAIN
|
||||
|
||||
RESOURCE_TYPE_NAMES = {
|
||||
# a simple mapping of hue resource type to Hass name
|
||||
|
@ -71,7 +71,7 @@ class HueBaseEntity(Entity):
|
|||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Call when entity is added."""
|
||||
self._check_availability_workaround()
|
||||
self._check_availability()
|
||||
# Add value_changed callbacks.
|
||||
self.async_on_remove(
|
||||
self.controller.subscribe(
|
||||
|
@ -80,7 +80,7 @@ class HueBaseEntity(Entity):
|
|||
(EventType.RESOURCE_UPDATED, EventType.RESOURCE_DELETED),
|
||||
)
|
||||
)
|
||||
# also subscribe to device update event to catch devicer changes (e.g. name)
|
||||
# also subscribe to device update event to catch device changes (e.g. name)
|
||||
if self.device is None:
|
||||
return
|
||||
self.async_on_remove(
|
||||
|
@ -92,25 +92,27 @@ class HueBaseEntity(Entity):
|
|||
)
|
||||
# subscribe to zigbee_connectivity to catch availability changes
|
||||
if zigbee := self.bridge.api.devices.get_zigbee_connectivity(self.device.id):
|
||||
self.bridge.api.sensors.zigbee_connectivity.subscribe(
|
||||
self._handle_event,
|
||||
zigbee.id,
|
||||
EventType.RESOURCE_UPDATED,
|
||||
self.async_on_remove(
|
||||
self.bridge.api.sensors.zigbee_connectivity.subscribe(
|
||||
self._handle_event,
|
||||
zigbee.id,
|
||||
EventType.RESOURCE_UPDATED,
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return entity availability."""
|
||||
# entities without a device attached should be always available
|
||||
if self.device is None:
|
||||
# entities without a device attached should be always available
|
||||
return True
|
||||
# the zigbee connectivity sensor itself should be always available
|
||||
if self.resource.type == ResourceTypes.ZIGBEE_CONNECTIVITY:
|
||||
# the zigbee connectivity sensor itself should be always available
|
||||
return True
|
||||
if self._ignore_availability:
|
||||
return True
|
||||
# all device-attached entities get availability from the zigbee connectivity
|
||||
if zigbee := self.bridge.api.devices.get_zigbee_connectivity(self.device.id):
|
||||
# all device-attached entities get availability from the zigbee connectivity
|
||||
return zigbee.status == ConnectivityServiceStatus.CONNECTED
|
||||
return True
|
||||
|
||||
|
@ -130,30 +132,41 @@ class HueBaseEntity(Entity):
|
|||
ent_reg.async_remove(self.entity_id)
|
||||
else:
|
||||
self.logger.debug("Received status update for %s", self.entity_id)
|
||||
self._check_availability_workaround()
|
||||
self._check_availability()
|
||||
self.on_update()
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def _check_availability_workaround(self):
|
||||
def _check_availability(self):
|
||||
"""Check availability of the device."""
|
||||
if self.resource.type != ResourceTypes.LIGHT:
|
||||
return
|
||||
# return if we already processed this entity
|
||||
if self._ignore_availability is not None:
|
||||
# already processed
|
||||
return
|
||||
# only do the availability check for entities connected to a device
|
||||
if self.device is None:
|
||||
return
|
||||
# ignore availability if user added device to ignore list
|
||||
if self.device.id in self.bridge.config_entry.options.get(
|
||||
CONF_IGNORE_AVAILABILITY, []
|
||||
):
|
||||
self._ignore_availability = True
|
||||
self.logger.info(
|
||||
"Device %s is configured to ignore availability status. ",
|
||||
self.name,
|
||||
)
|
||||
return
|
||||
# certified products (normally) report their state correctly
|
||||
# no need for workaround/reporting
|
||||
if self.device.product_data.certified:
|
||||
# certified products report their state correctly
|
||||
self._ignore_availability = False
|
||||
return
|
||||
# some (3th party) Hue lights report their connection status incorrectly
|
||||
# causing the zigbee availability to report as disconnected while in fact
|
||||
# it can be controlled. Although this is in fact something the device manufacturer
|
||||
# should fix, we work around it here. If the light is reported unavailable
|
||||
# it can be controlled. If the light is reported unavailable
|
||||
# by the zigbee connectivity but the state changes its considered as a
|
||||
# malfunctioning device and we report it.
|
||||
# while the user should actually fix this issue instead of ignoring it, we
|
||||
# ignore the availability for this light from this point.
|
||||
# While the user should actually fix this issue, we allow to
|
||||
# ignore the availability for this light/device from the config options.
|
||||
cur_state = self.resource.on.on
|
||||
if self._last_state is None:
|
||||
self._last_state = cur_state
|
||||
|
@ -166,9 +179,10 @@ class HueBaseEntity(Entity):
|
|||
# the device state changed from on->off or off->on
|
||||
# while it was reported as not connected!
|
||||
self.logger.warning(
|
||||
"Light %s changed state while reported as disconnected. "
|
||||
"This might be an indicator that routing is not working for this device. "
|
||||
"Home Assistant will ignore availability for this light from now on. "
|
||||
"Device %s changed state while reported as disconnected. "
|
||||
"This might be an indicator that routing is not working for this device "
|
||||
"or the device is having connectivity issues. "
|
||||
"You can disable availability reporting for this device in the Hue options. "
|
||||
"Device details: %s - %s (%s) fw: %s",
|
||||
self.name,
|
||||
self.device.product_data.manufacturer_name,
|
||||
|
@ -178,6 +192,4 @@ class HueBaseEntity(Entity):
|
|||
)
|
||||
# do we want to store this in some persistent storage?
|
||||
self._ignore_availability = True
|
||||
else:
|
||||
self._ignore_availability = False
|
||||
self._last_state = cur_state
|
||||
|
|
|
@ -102,7 +102,7 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
|
|||
|
||||
# Entities for Hue groups are disabled by default
|
||||
# unless they were enabled in old version (legacy option)
|
||||
self._attr_entity_registry_enabled_default = bridge.config_entry.data.get(
|
||||
self._attr_entity_registry_enabled_default = bridge.config_entry.options.get(
|
||||
CONF_ALLOW_HUE_GROUPS, False
|
||||
)
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ from homeassistant import config_entries
|
|||
from homeassistant.components import ssdp, zeroconf
|
||||
from homeassistant.components.hue import config_flow, const
|
||||
from homeassistant.components.hue.errors import CannotConnect
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
@ -701,12 +702,33 @@ async def test_options_flow_v2(hass):
|
|||
"""Test options config flow for a V2 bridge."""
|
||||
entry = MockConfigEntry(
|
||||
domain="hue",
|
||||
unique_id="v2bridge",
|
||||
unique_id="aabbccddeeff",
|
||||
data={"host": "0.0.0.0", "api_version": 2},
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
assert config_flow.HueFlowHandler.async_supports_options_flow(entry) is False
|
||||
dev_reg = dr.async_get(hass)
|
||||
mock_dev_id = "aabbccddee"
|
||||
dev_reg.async_get_or_create(
|
||||
config_entry_id=entry.entry_id, identifiers={(const.DOMAIN, mock_dev_id)}
|
||||
)
|
||||
|
||||
result = await hass.config_entries.options.async_init(entry.entry_id)
|
||||
|
||||
assert result["type"] == "form"
|
||||
assert result["step_id"] == "init"
|
||||
schema = result["data_schema"].schema
|
||||
assert _get_schema_default(schema, const.CONF_IGNORE_AVAILABILITY) == []
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={const.CONF_IGNORE_AVAILABILITY: [mock_dev_id]},
|
||||
)
|
||||
|
||||
assert result["type"] == "create_entry"
|
||||
assert result["data"] == {
|
||||
const.CONF_IGNORE_AVAILABILITY: [mock_dev_id],
|
||||
}
|
||||
|
||||
|
||||
async def test_bridge_zeroconf(hass, aioclient_mock):
|
||||
|
|
Loading…
Reference in New Issue