1
mirror of https://github.com/home-assistant/core synced 2024-08-02 23:40:32 +02:00

Hue allow per-device availability override (#63025)

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
This commit is contained in:
Marcel van der Veldt 2021-12-31 05:46:52 +01:00 committed by GitHub
parent ebe9853e6f
commit 055fb99938
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 122 additions and 44 deletions

View File

@ -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),
}
),
)

View File

@ -3,6 +3,7 @@
DOMAIN = "hue"
CONF_API_VERSION = "api_version"
CONF_IGNORE_AVAILABILITY = "ignore_availability"
CONF_SUBTYPE = "subtype"

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}

View File

@ -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

View File

@ -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
)

View File

@ -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):