mirror of
https://github.com/home-assistant/core.git
synced 2026-08-28 10:16:02 -05:00
Cleanup broken switch entities for Hue automations (#177671)
Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>
This commit is contained in:
co-authored by
Jan Bouwhuis
parent
92d5741fec
commit
b00ff0bb11
@@ -1,5 +1,6 @@
|
||||
"""Support for switch platform for Hue resources (V2 only)."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any, override
|
||||
|
||||
from aiohue.v2 import HueBridgeV2
|
||||
@@ -11,17 +12,20 @@ from aiohue.v2.controllers.sensors import (
|
||||
Motion,
|
||||
MotionController,
|
||||
)
|
||||
from aiohue.v2.models.behavior_script import BehaviorScriptCategory
|
||||
|
||||
from homeassistant.components.switch import (
|
||||
SwitchDeviceClass,
|
||||
SwitchEntity,
|
||||
SwitchEntityDescription,
|
||||
)
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.const import EntityCategory, Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .bridge import HueConfigEntry
|
||||
from .const import DOMAIN
|
||||
from .v2.entity import HueBaseEntity
|
||||
|
||||
|
||||
@@ -48,12 +52,15 @@ async def async_setup_entry(
|
||||
| HueLightSensorEnabledEntity
|
||||
| HueMotionSensorEnabledEntity
|
||||
],
|
||||
resource_filter: Callable[[Any], bool] | None = None,
|
||||
):
|
||||
@callback
|
||||
def async_add_entity(
|
||||
event_type: EventType, resource: BehaviorInstance | LightLevel | Motion
|
||||
) -> None:
|
||||
"""Add entity from Hue resource."""
|
||||
if resource_filter is not None and not resource_filter(resource):
|
||||
return
|
||||
async_add_entities([switch_class(bridge, controller, resource)])
|
||||
|
||||
# add all current items in controller
|
||||
@@ -67,10 +74,38 @@ async def async_setup_entry(
|
||||
)
|
||||
)
|
||||
|
||||
@callback
|
||||
def is_user_automation(resource: BehaviorInstance) -> bool:
|
||||
"""Return if the behavior instance is an automation from the Hue app.
|
||||
|
||||
Anything else is device configuration, which the bridge keeps running
|
||||
even after it accepts switching it off. Categories we do not recognise
|
||||
are skipped too, better no switch than one that does nothing.
|
||||
"""
|
||||
script = api.config.behavior_script.get(resource.script_id)
|
||||
return (
|
||||
script is not None
|
||||
and script.metadata.category is BehaviorScriptCategory.AUTOMATION
|
||||
)
|
||||
|
||||
# clean up entities previously created for internal behavior instances
|
||||
entity_registry = er.async_get(hass)
|
||||
for resource in api.config.behavior_instance:
|
||||
if is_user_automation(resource):
|
||||
continue
|
||||
if entity_id := entity_registry.async_get_entity_id(
|
||||
Platform.SWITCH, DOMAIN, resource.id
|
||||
):
|
||||
entity_registry.async_remove(entity_id)
|
||||
|
||||
# setup for each switch-type hue resource
|
||||
register_items(api.sensors.motion, HueMotionSensorEnabledEntity)
|
||||
register_items(api.sensors.light_level, HueLightSensorEnabledEntity)
|
||||
register_items(api.config.behavior_instance, HueBehaviorInstanceEnabledEntity)
|
||||
register_items(
|
||||
api.config.behavior_instance,
|
||||
HueBehaviorInstanceEnabledEntity,
|
||||
is_user_automation,
|
||||
)
|
||||
|
||||
|
||||
class HueResourceEnabledEntity(HueBaseEntity, SwitchEntity):
|
||||
|
||||
@@ -138,3 +138,27 @@ FAKE_ROTARY = {
|
||||
},
|
||||
"type": "relative_rotary",
|
||||
}
|
||||
|
||||
FAKE_BEHAVIOR_SCRIPT = {
|
||||
"configuration_schema": {},
|
||||
"description": "Generic switches script",
|
||||
"id": "fake_behavior_script_id_1",
|
||||
"metadata": {"category": "accessory", "name": "Hue Accessories"},
|
||||
"state_schema": {},
|
||||
"supported_features": [],
|
||||
"trigger_schema": {},
|
||||
"type": "behavior_script",
|
||||
"version": "0.0.1",
|
||||
}
|
||||
|
||||
FAKE_BEHAVIOR_INSTANCE = {
|
||||
"configuration": {},
|
||||
"dependees": [], # codespell:ignore dependees
|
||||
"enabled": True,
|
||||
"id": "fake_behavior_instance_id_1",
|
||||
"last_error": "",
|
||||
"metadata": {"name": "Wall switch Hallway"},
|
||||
"script_id": "fake_behavior_script_id_1",
|
||||
"status": "running",
|
||||
"type": "behavior_instance",
|
||||
}
|
||||
|
||||
@@ -2235,6 +2235,7 @@
|
||||
"description": "Countdown Timer",
|
||||
"id": "e73bc72d-96b1-46f8-aa57-729861f80c78",
|
||||
"metadata": {
|
||||
"category": "automation",
|
||||
"name": "Timers"
|
||||
},
|
||||
"state_schema": {
|
||||
|
||||
@@ -2,12 +2,22 @@
|
||||
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.hue.const import DOMAIN
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.util.json import JsonArrayType
|
||||
|
||||
from .conftest import setup_platform
|
||||
from .const import FAKE_BINARY_SENSOR, FAKE_DEVICE, FAKE_ZIGBEE_CONNECTIVITY
|
||||
from .const import (
|
||||
FAKE_BEHAVIOR_INSTANCE,
|
||||
FAKE_BEHAVIOR_SCRIPT,
|
||||
FAKE_BINARY_SENSOR,
|
||||
FAKE_DEVICE,
|
||||
FAKE_ZIGBEE_CONNECTIVITY,
|
||||
)
|
||||
|
||||
|
||||
async def test_switch(
|
||||
@@ -131,3 +141,60 @@ async def test_switch_added(hass: HomeAssistant, mock_bridge_v2: Mock) -> None:
|
||||
test_entity = hass.states.get(test_entity_id)
|
||||
assert test_entity is not None
|
||||
assert test_entity.state == "off"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"metadata",
|
||||
[
|
||||
pytest.param(
|
||||
{"name": "Hue Accessories", "category": "accessory"}, id="accessory"
|
||||
),
|
||||
pytest.param(
|
||||
{"name": "Light state after streaming", "category": "entertainment"},
|
||||
id="entertainment",
|
||||
),
|
||||
pytest.param({"name": "Old bridge script"}, id="no_category"),
|
||||
],
|
||||
)
|
||||
async def test_internal_behavior_instance_not_added(
|
||||
hass: HomeAssistant,
|
||||
mock_bridge_v2: Mock,
|
||||
v2_resources_test_data: JsonArrayType,
|
||||
metadata: dict,
|
||||
) -> None:
|
||||
"""Test internal behavior instances are not exposed as switches.
|
||||
|
||||
The bridge accepts a change to `enabled` on these but keeps running them,
|
||||
so a switch for them would silently do nothing. Bridges that do not report
|
||||
a category at all are skipped for the same reason.
|
||||
"""
|
||||
internal_script = {**FAKE_BEHAVIOR_SCRIPT, "metadata": metadata}
|
||||
await mock_bridge_v2.api.load_test_data(
|
||||
[*v2_resources_test_data, internal_script, FAKE_BEHAVIOR_INSTANCE]
|
||||
)
|
||||
|
||||
await setup_platform(hass, mock_bridge_v2, Platform.SWITCH)
|
||||
|
||||
assert hass.states.get("switch.philips_hue_automation_wall_switch_hallway") is None
|
||||
assert hass.states.get("switch.philips_hue_automation_timer_test") is not None
|
||||
assert len(hass.states.async_all()) == 4
|
||||
|
||||
|
||||
async def test_internal_behavior_instance_entity_removed(
|
||||
hass: HomeAssistant,
|
||||
mock_bridge_v2: Mock,
|
||||
v2_resources_test_data: JsonArrayType,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test a previously created entity for an internal instance is removed."""
|
||||
# Simulate an entity created with a previous version of the integration
|
||||
stale_entity = entity_registry.async_get_or_create(
|
||||
Platform.SWITCH, DOMAIN, FAKE_BEHAVIOR_INSTANCE["id"]
|
||||
)
|
||||
await mock_bridge_v2.api.load_test_data(
|
||||
[*v2_resources_test_data, FAKE_BEHAVIOR_SCRIPT, FAKE_BEHAVIOR_INSTANCE]
|
||||
)
|
||||
|
||||
await setup_platform(hass, mock_bridge_v2, Platform.SWITCH)
|
||||
|
||||
assert entity_registry.async_get(stale_entity.entity_id) is None
|
||||
|
||||
Reference in New Issue
Block a user