mirror of
https://github.com/home-assistant/core.git
synced 2025-07-24 21:57:51 +00:00
Hue allow per-device availability override (#63025)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
This commit is contained in:
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.const import CONF_API_KEY, CONF_HOST
|
||||||
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 aiohttp_client
|
from homeassistant.helpers import aiohttp_client, device_registry
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_ALLOW_HUE_GROUPS,
|
CONF_ALLOW_HUE_GROUPS,
|
||||||
CONF_ALLOW_UNREACHABLE,
|
CONF_ALLOW_UNREACHABLE,
|
||||||
CONF_API_VERSION,
|
CONF_API_VERSION,
|
||||||
|
CONF_IGNORE_AVAILABILITY,
|
||||||
DEFAULT_ALLOW_HUE_GROUPS,
|
DEFAULT_ALLOW_HUE_GROUPS,
|
||||||
DEFAULT_ALLOW_UNREACHABLE,
|
DEFAULT_ALLOW_UNREACHABLE,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
@ -46,17 +48,11 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
@callback
|
@callback
|
||||||
def async_get_options_flow(
|
def async_get_options_flow(
|
||||||
config_entry: config_entries.ConfigEntry,
|
config_entry: config_entries.ConfigEntry,
|
||||||
) -> HueOptionsFlowHandler:
|
) -> HueV1OptionsFlowHandler | HueV2OptionsFlowHandler:
|
||||||
"""Get the options flow for this handler."""
|
"""Get the options flow for this handler."""
|
||||||
return HueOptionsFlowHandler(config_entry)
|
if config_entry.data.get(CONF_API_VERSION, 1) == 1:
|
||||||
|
return HueV1OptionsFlowHandler(config_entry)
|
||||||
@classmethod
|
return HueV2OptionsFlowHandler(config_entry)
|
||||||
@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
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
"""Initialize the Hue flow."""
|
"""Initialize the Hue flow."""
|
||||||
@ -288,8 +284,8 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
return await self.async_step_link()
|
return await self.async_step_link()
|
||||||
|
|
||||||
|
|
||||||
class HueOptionsFlowHandler(config_entries.OptionsFlow):
|
class HueV1OptionsFlowHandler(config_entries.OptionsFlow):
|
||||||
"""Handle Hue options."""
|
"""Handle Hue options for V1 implementation."""
|
||||||
|
|
||||||
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
|
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
|
||||||
"""Initialize Hue options flow."""
|
"""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"
|
DOMAIN = "hue"
|
||||||
|
|
||||||
CONF_API_VERSION = "api_version"
|
CONF_API_VERSION = "api_version"
|
||||||
|
CONF_IGNORE_AVAILABILITY = "ignore_availability"
|
||||||
|
|
||||||
CONF_SUBTYPE = "subtype"
|
CONF_SUBTYPE = "subtype"
|
||||||
|
|
||||||
|
@ -70,7 +70,8 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"allow_hue_groups": "Allow Hue groups",
|
"allow_hue_groups": "Allow Hue groups",
|
||||||
"allow_hue_scenes": "Allow Hue scenes",
|
"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": {
|
"data": {
|
||||||
"allow_hue_groups": "Allow Hue groups",
|
"allow_hue_groups": "Allow Hue groups",
|
||||||
"allow_hue_scenes": "Allow Hue scenes",
|
"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": {
|
"data": {
|
||||||
"allow_hue_groups": "Sta Hue-groepen toe",
|
"allow_hue_groups": "Sta Hue-groepen toe",
|
||||||
"allow_hue_scenes": "Sta Hue sc\u00e8nes 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 homeassistant.helpers.entity_registry import async_get as async_get_entity_registry
|
||||||
|
|
||||||
from ..bridge import HueBridge
|
from ..bridge import HueBridge
|
||||||
from ..const import DOMAIN
|
from ..const import CONF_IGNORE_AVAILABILITY, DOMAIN
|
||||||
|
|
||||||
RESOURCE_TYPE_NAMES = {
|
RESOURCE_TYPE_NAMES = {
|
||||||
# a simple mapping of hue resource type to Hass name
|
# 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:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""Call when entity is added."""
|
"""Call when entity is added."""
|
||||||
self._check_availability_workaround()
|
self._check_availability()
|
||||||
# Add value_changed callbacks.
|
# Add value_changed callbacks.
|
||||||
self.async_on_remove(
|
self.async_on_remove(
|
||||||
self.controller.subscribe(
|
self.controller.subscribe(
|
||||||
@ -80,7 +80,7 @@ class HueBaseEntity(Entity):
|
|||||||
(EventType.RESOURCE_UPDATED, EventType.RESOURCE_DELETED),
|
(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:
|
if self.device is None:
|
||||||
return
|
return
|
||||||
self.async_on_remove(
|
self.async_on_remove(
|
||||||
@ -92,25 +92,27 @@ class HueBaseEntity(Entity):
|
|||||||
)
|
)
|
||||||
# subscribe to zigbee_connectivity to catch availability changes
|
# subscribe to zigbee_connectivity to catch availability changes
|
||||||
if zigbee := self.bridge.api.devices.get_zigbee_connectivity(self.device.id):
|
if zigbee := self.bridge.api.devices.get_zigbee_connectivity(self.device.id):
|
||||||
self.bridge.api.sensors.zigbee_connectivity.subscribe(
|
self.async_on_remove(
|
||||||
self._handle_event,
|
self.bridge.api.sensors.zigbee_connectivity.subscribe(
|
||||||
zigbee.id,
|
self._handle_event,
|
||||||
EventType.RESOURCE_UPDATED,
|
zigbee.id,
|
||||||
|
EventType.RESOURCE_UPDATED,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def available(self) -> bool:
|
def available(self) -> bool:
|
||||||
"""Return entity availability."""
|
"""Return entity availability."""
|
||||||
|
# entities without a device attached should be always available
|
||||||
if self.device is None:
|
if self.device is None:
|
||||||
# entities without a device attached should be always available
|
|
||||||
return True
|
return True
|
||||||
|
# the zigbee connectivity sensor itself should be always available
|
||||||
if self.resource.type == ResourceTypes.ZIGBEE_CONNECTIVITY:
|
if self.resource.type == ResourceTypes.ZIGBEE_CONNECTIVITY:
|
||||||
# the zigbee connectivity sensor itself should be always available
|
|
||||||
return True
|
return True
|
||||||
if self._ignore_availability:
|
if self._ignore_availability:
|
||||||
return True
|
return True
|
||||||
|
# all device-attached entities get availability from the zigbee connectivity
|
||||||
if zigbee := self.bridge.api.devices.get_zigbee_connectivity(self.device.id):
|
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 zigbee.status == ConnectivityServiceStatus.CONNECTED
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -130,30 +132,41 @@ class HueBaseEntity(Entity):
|
|||||||
ent_reg.async_remove(self.entity_id)
|
ent_reg.async_remove(self.entity_id)
|
||||||
else:
|
else:
|
||||||
self.logger.debug("Received status update for %s", self.entity_id)
|
self.logger.debug("Received status update for %s", self.entity_id)
|
||||||
self._check_availability_workaround()
|
self._check_availability()
|
||||||
self.on_update()
|
self.on_update()
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _check_availability_workaround(self):
|
def _check_availability(self):
|
||||||
"""Check availability of the device."""
|
"""Check availability of the device."""
|
||||||
if self.resource.type != ResourceTypes.LIGHT:
|
# return if we already processed this entity
|
||||||
return
|
|
||||||
if self._ignore_availability is not None:
|
if self._ignore_availability is not None:
|
||||||
# already processed
|
|
||||||
return
|
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:
|
if self.device.product_data.certified:
|
||||||
# certified products report their state correctly
|
|
||||||
self._ignore_availability = False
|
self._ignore_availability = False
|
||||||
return
|
return
|
||||||
# some (3th party) Hue lights report their connection status incorrectly
|
# some (3th party) Hue lights report their connection status incorrectly
|
||||||
# causing the zigbee availability to report as disconnected while in fact
|
# causing the zigbee availability to report as disconnected while in fact
|
||||||
# it can be controlled. Although this is in fact something the device manufacturer
|
# it can be controlled. If the light is reported unavailable
|
||||||
# should fix, we work around it here. If the light is reported unavailable
|
|
||||||
# by the zigbee connectivity but the state changes its considered as a
|
# by the zigbee connectivity but the state changes its considered as a
|
||||||
# malfunctioning device and we report it.
|
# malfunctioning device and we report it.
|
||||||
# while the user should actually fix this issue instead of ignoring it, we
|
# While the user should actually fix this issue, we allow to
|
||||||
# ignore the availability for this light from this point.
|
# ignore the availability for this light/device from the config options.
|
||||||
cur_state = self.resource.on.on
|
cur_state = self.resource.on.on
|
||||||
if self._last_state is None:
|
if self._last_state is None:
|
||||||
self._last_state = cur_state
|
self._last_state = cur_state
|
||||||
@ -166,9 +179,10 @@ class HueBaseEntity(Entity):
|
|||||||
# the device state changed from on->off or off->on
|
# the device state changed from on->off or off->on
|
||||||
# while it was reported as not connected!
|
# while it was reported as not connected!
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
"Light %s changed state while reported as disconnected. "
|
"Device %s changed state while reported as disconnected. "
|
||||||
"This might be an indicator that routing is not working for this device. "
|
"This might be an indicator that routing is not working for this device "
|
||||||
"Home Assistant will ignore availability for this light from now on. "
|
"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",
|
"Device details: %s - %s (%s) fw: %s",
|
||||||
self.name,
|
self.name,
|
||||||
self.device.product_data.manufacturer_name,
|
self.device.product_data.manufacturer_name,
|
||||||
@ -178,6 +192,4 @@ class HueBaseEntity(Entity):
|
|||||||
)
|
)
|
||||||
# do we want to store this in some persistent storage?
|
# do we want to store this in some persistent storage?
|
||||||
self._ignore_availability = True
|
self._ignore_availability = True
|
||||||
else:
|
|
||||||
self._ignore_availability = False
|
|
||||||
self._last_state = cur_state
|
self._last_state = cur_state
|
||||||
|
@ -102,7 +102,7 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
|
|||||||
|
|
||||||
# Entities for Hue groups are disabled by default
|
# Entities for Hue groups are disabled by default
|
||||||
# unless they were enabled in old version (legacy option)
|
# 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
|
CONF_ALLOW_HUE_GROUPS, False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ from homeassistant import config_entries
|
|||||||
from homeassistant.components import ssdp, zeroconf
|
from homeassistant.components import ssdp, zeroconf
|
||||||
from homeassistant.components.hue import config_flow, const
|
from homeassistant.components.hue import config_flow, const
|
||||||
from homeassistant.components.hue.errors import CannotConnect
|
from homeassistant.components.hue.errors import CannotConnect
|
||||||
|
from homeassistant.helpers import device_registry as dr
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
@ -701,12 +702,33 @@ async def test_options_flow_v2(hass):
|
|||||||
"""Test options config flow for a V2 bridge."""
|
"""Test options config flow for a V2 bridge."""
|
||||||
entry = MockConfigEntry(
|
entry = MockConfigEntry(
|
||||||
domain="hue",
|
domain="hue",
|
||||||
unique_id="v2bridge",
|
unique_id="aabbccddeeff",
|
||||||
data={"host": "0.0.0.0", "api_version": 2},
|
data={"host": "0.0.0.0", "api_version": 2},
|
||||||
)
|
)
|
||||||
entry.add_to_hass(hass)
|
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):
|
async def test_bridge_zeroconf(hass, aioclient_mock):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user