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

View file

@ -3,6 +3,10 @@ from pydeconz.utils import normalize_bridge_id
import voluptuous as vol import voluptuous as vol
from homeassistant.helpers import config_validation as cv 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 .config_flow import get_master_gateway
from .const import ( from .const import (
@ -35,7 +39,8 @@ SERVICE_CONFIGURE_DEVICE_SCHEMA = vol.All(
) )
SERVICE_DEVICE_REFRESH = "device_refresh" 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): async def async_setup_services(hass):
@ -56,6 +61,9 @@ async def async_setup_services(hass):
elif service == SERVICE_DEVICE_REFRESH: elif service == SERVICE_DEVICE_REFRESH:
await async_refresh_devices_service(hass, service_data) 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( hass.services.async_register(
DOMAIN, DOMAIN,
SERVICE_CONFIGURE_DEVICE, SERVICE_CONFIGURE_DEVICE,
@ -67,7 +75,14 @@ async def async_setup_services(hass):
DOMAIN, DOMAIN,
SERVICE_DEVICE_REFRESH, SERVICE_DEVICE_REFRESH,
async_call_deconz_service, 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_CONFIGURE_DEVICE)
hass.services.async_remove(DOMAIN, SERVICE_DEVICE_REFRESH) hass.services.async_remove(DOMAIN, SERVICE_DEVICE_REFRESH)
hass.services.async_remove(DOMAIN, SERVICE_REMOVE_ORPHANED_ENTRIES)
async def async_configure_service(hass, data): 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 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: bridgeid:
description: (Optional) Bridgeid is a string unique for each deCONZ hardware. It can be found as part of the integration name. description: (Optional) Bridgeid is a string unique for each deCONZ hardware. It can be found as part of the integration name.
example: "00212EFFFF012345" 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.""" """deCONZ service tests."""
from copy import deepcopy
import pytest import pytest
import voluptuous as vol import voluptuous as vol
from homeassistant.components import deconz from homeassistant.components import deconz
from homeassistant.components.deconz.const import CONF_BRIDGE_ID 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 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): async def test_service_setup(hass):
"""Verify service setup works.""" """Verify service setup works."""
@ -52,7 +67,7 @@ async def test_service_setup(hass):
) as async_register: ) as async_register:
await deconz.services.async_setup_services(hass) await deconz.services.async_setup_services(hass)
assert hass.data[deconz.services.DECONZ_SERVICES] is True 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): async def test_service_setup_already_registered(hass):
@ -73,7 +88,7 @@ async def test_service_unload(hass):
) as async_remove: ) as async_remove:
await deconz.services.async_unload_services(hass) await deconz.services.async_unload_services(hass)
assert hass.data[deconz.services.DECONZ_SERVICES] is False 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): 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", "scene.group_1_name_scene_1": "/groups/1/scenes/1",
"sensor.sensor_1_name": "/sensors/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
)