Add deCONZ service to clean up orphaned device and entity entries (#40629)

* Add service that can clean up by deCONZ orphaned entries from device and entity registry

* Fix existing tests

* Add new test

* Add some inline comment to the service
This commit is contained in:
Robert Svensson 2020-09-30 20:11:41 +02:00 committed by GitHub
parent 9bdc716e60
commit 9d56d3bb31
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 169 additions and 6 deletions

View file

@ -51,6 +51,7 @@ class DeconzGateway:
self.ignore_state_updates = False
self.deconz_ids = {}
self.device_id = None
self.entities = {}
self.events = []
self.listeners = []
@ -139,7 +140,7 @@ class DeconzGateway:
async def async_update_device_registry(self) -> None:
"""Update device registry."""
device_registry = await self.hass.helpers.device_registry.async_get_registry()
device_registry.async_get_or_create(
entry = device_registry.async_get_or_create(
config_entry_id=self.config_entry.entry_id,
connections={(CONNECTION_NETWORK_MAC, self.api.config.mac)},
identifiers={(DOMAIN, self.api.config.bridgeid)},
@ -148,6 +149,7 @@ class DeconzGateway:
name=self.api.config.name,
sw_version=self.api.config.swversion,
)
self.device_id = entry.id
async def async_setup(self) -> bool:
"""Set up a deCONZ gateway."""

View file

@ -3,6 +3,10 @@ from pydeconz.utils import normalize_bridge_id
import voluptuous as vol
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_registry import (
async_entries_for_config_entry,
async_entries_for_device,
)
from .config_flow import get_master_gateway
from .const import (
@ -35,7 +39,8 @@ SERVICE_CONFIGURE_DEVICE_SCHEMA = vol.All(
)
SERVICE_DEVICE_REFRESH = "device_refresh"
SERVICE_DEVICE_REFRESH_SCHEMA = vol.All(vol.Schema({vol.Optional(CONF_BRIDGE_ID): str}))
SERVICE_REMOVE_ORPHANED_ENTRIES = "remove_orphaned_entries"
SELECT_GATEWAY_SCHEMA = vol.All(vol.Schema({vol.Optional(CONF_BRIDGE_ID): str}))
async def async_setup_services(hass):
@ -56,6 +61,9 @@ async def async_setup_services(hass):
elif service == SERVICE_DEVICE_REFRESH:
await async_refresh_devices_service(hass, service_data)
elif service == SERVICE_REMOVE_ORPHANED_ENTRIES:
await async_remove_orphaned_entries_service(hass, service_data)
hass.services.async_register(
DOMAIN,
SERVICE_CONFIGURE_DEVICE,
@ -67,7 +75,14 @@ async def async_setup_services(hass):
DOMAIN,
SERVICE_DEVICE_REFRESH,
async_call_deconz_service,
schema=SERVICE_DEVICE_REFRESH_SCHEMA,
schema=SELECT_GATEWAY_SCHEMA,
)
hass.services.async_register(
DOMAIN,
SERVICE_REMOVE_ORPHANED_ENTRIES,
async_call_deconz_service,
schema=SELECT_GATEWAY_SCHEMA,
)
@ -80,6 +95,7 @@ async def async_unload_services(hass):
hass.services.async_remove(DOMAIN, SERVICE_CONFIGURE_DEVICE)
hass.services.async_remove(DOMAIN, SERVICE_DEVICE_REFRESH)
hass.services.async_remove(DOMAIN, SERVICE_REMOVE_ORPHANED_ENTRIES)
async def async_configure_service(hass, data):
@ -166,3 +182,54 @@ async def async_refresh_devices_service(hass, data):
if sensor_id not in sensors
],
)
async def async_remove_orphaned_entries_service(hass, data):
"""Remove orphaned deCONZ entries from device and entity registries."""
gateway = get_master_gateway(hass)
if CONF_BRIDGE_ID in data:
gateway = hass.data[DOMAIN][normalize_bridge_id(data[CONF_BRIDGE_ID])]
entity_registry = await hass.helpers.entity_registry.async_get_registry()
device_registry = await hass.helpers.device_registry.async_get_registry()
entity_entries = async_entries_for_config_entry(
entity_registry, gateway.config_entry.entry_id
)
entities_to_be_removed = []
devices_to_be_removed = [
entry.id
for entry in device_registry.devices.values()
if gateway.config_entry.entry_id in entry.config_entries
]
# Don't remove the Gateway device
if gateway.device_id in devices_to_be_removed:
devices_to_be_removed.remove(gateway.device_id)
# Don't remove devices belonging to available events
for event in gateway.events:
if event.device_id in devices_to_be_removed:
devices_to_be_removed.remove(event.device_id)
for entry in entity_entries:
# Don't remove available entities
if entry.unique_id in gateway.entities[entry.domain]:
# Don't remove devices with available entities
if entry.device_id in devices_to_be_removed:
devices_to_be_removed.remove(entry.device_id)
continue
# Remove entities that are not available
entities_to_be_removed.append(entry.entity_id)
# Remove unavailable entities
for entity_id in entities_to_be_removed:
entity_registry.async_remove(entity_id)
# Remove devices that don't belong to any entity
for device_id in devices_to_be_removed:
if len(async_entries_for_device(entity_registry, device_id)) == 0:
device_registry.async_remove_device(device_id)

View file

@ -23,3 +23,10 @@ device_refresh:
bridgeid:
description: (Optional) Bridgeid is a string unique for each deCONZ hardware. It can be found as part of the integration name.
example: "00212EFFFF012345"
remove_orphaned_entries:
description: Clean up device and entity registry entries orphaned by deCONZ.
fields:
bridgeid:
description: (Optional) Bridgeid is a string unique for each deCONZ hardware. It can be found as part of the integration name.
example: "00212EFFFF012345"

View file

@ -1,12 +1,16 @@
"""deCONZ service tests."""
from copy import deepcopy
import pytest
import voluptuous as vol
from homeassistant.components import deconz
from homeassistant.components.deconz.const import CONF_BRIDGE_ID
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.helpers.entity_registry import async_entries_for_config_entry
from .test_gateway import BRIDGEID, setup_deconz_integration
from .test_gateway import BRIDGEID, DECONZ_WEB_REQUEST, setup_deconz_integration
from tests.async_mock import Mock, patch
@ -43,6 +47,17 @@ SENSOR = {
}
}
SWITCH = {
"1": {
"id": "Switch 1 id",
"name": "Switch 1",
"type": "ZHASwitch",
"state": {"buttonevent": 1000, "gesture": 1},
"config": {"battery": 100},
"uniqueid": "00:00:00:00:00:00:00:03-00",
},
}
async def test_service_setup(hass):
"""Verify service setup works."""
@ -52,7 +67,7 @@ async def test_service_setup(hass):
) as async_register:
await deconz.services.async_setup_services(hass)
assert hass.data[deconz.services.DECONZ_SERVICES] is True
assert async_register.call_count == 2
assert async_register.call_count == 3
async def test_service_setup_already_registered(hass):
@ -73,7 +88,7 @@ async def test_service_unload(hass):
) as async_remove:
await deconz.services.async_unload_services(hass)
assert hass.data[deconz.services.DECONZ_SERVICES] is False
assert async_remove.call_count == 2
assert async_remove.call_count == 3
async def test_service_unload_not_registered(hass):
@ -198,3 +213,75 @@ async def test_service_refresh_devices(hass):
"scene.group_1_name_scene_1": "/groups/1/scenes/1",
"sensor.sensor_1_name": "/sensors/1",
}
async def test_remove_orphaned_entries_service(hass):
"""Test service works and also don't remove more than expected."""
data = deepcopy(DECONZ_WEB_REQUEST)
data["lights"] = deepcopy(LIGHT)
data["sensors"] = deepcopy(SWITCH)
gateway = await setup_deconz_integration(hass, get_state_response=data)
data = {CONF_BRIDGE_ID: BRIDGEID}
device_registry = await hass.helpers.device_registry.async_get_registry()
device = device_registry.async_get_or_create(
config_entry_id=gateway.config_entry.entry_id, identifiers={("mac", "123")}
)
assert (
len(
[
entry
for entry in device_registry.devices.values()
if gateway.config_entry.entry_id in entry.config_entries
]
)
== 4 # Gateway, light, switch and orphan
)
entity_registry = await hass.helpers.entity_registry.async_get_registry()
entity_registry.async_get_or_create(
SENSOR_DOMAIN,
deconz.DOMAIN,
"12345",
suggested_object_id="Orphaned sensor",
config_entry=gateway.config_entry,
device_id=device.id,
)
assert (
len(
async_entries_for_config_entry(
entity_registry, gateway.config_entry.entry_id
)
)
== 3 # Light, switch battery and orphan
)
await hass.services.async_call(
deconz.DOMAIN,
deconz.services.SERVICE_REMOVE_ORPHANED_ENTRIES,
service_data=data,
)
await hass.async_block_till_done()
assert (
len(
[
entry
for entry in device_registry.devices.values()
if gateway.config_entry.entry_id in entry.config_entries
]
)
== 3 # Gateway, light and switch
)
assert (
len(
async_entries_for_config_entry(
entity_registry, gateway.config_entry.entry_id
)
)
== 2 # Light and switch battery
)