"""deCONZ services.""" import asyncio from pydeconz.utils import normalize_bridge_id import voluptuous as vol from homeassistant.helpers import config_validation as cv from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC 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 ( CONF_BRIDGE_ID, DOMAIN, LOGGER, NEW_GROUP, NEW_LIGHT, NEW_SCENE, NEW_SENSOR, ) DECONZ_SERVICES = "deconz_services" SERVICE_FIELD = "field" SERVICE_ENTITY = "entity" SERVICE_DATA = "data" SERVICE_CONFIGURE_DEVICE = "configure" SERVICE_CONFIGURE_DEVICE_SCHEMA = vol.All( vol.Schema( { vol.Optional(SERVICE_ENTITY): cv.entity_id, vol.Optional(SERVICE_FIELD): cv.matches_regex("/.*"), vol.Required(SERVICE_DATA): dict, vol.Optional(CONF_BRIDGE_ID): str, } ), cv.has_at_least_one_key(SERVICE_ENTITY, SERVICE_FIELD), ) SERVICE_DEVICE_REFRESH = "device_refresh" 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): """Set up services for deCONZ integration.""" if hass.data.get(DECONZ_SERVICES, False): return hass.data[DECONZ_SERVICES] = True async def async_call_deconz_service(service_call): """Call correct deCONZ service.""" service = service_call.service service_data = service_call.data if service == SERVICE_CONFIGURE_DEVICE: await async_configure_service(hass, service_data) 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, async_call_deconz_service, schema=SERVICE_CONFIGURE_DEVICE_SCHEMA, ) hass.services.async_register( DOMAIN, SERVICE_DEVICE_REFRESH, async_call_deconz_service, schema=SELECT_GATEWAY_SCHEMA, ) hass.services.async_register( DOMAIN, SERVICE_REMOVE_ORPHANED_ENTRIES, async_call_deconz_service, schema=SELECT_GATEWAY_SCHEMA, ) async def async_unload_services(hass): """Unload deCONZ services.""" if not hass.data.get(DECONZ_SERVICES): return hass.data[DECONZ_SERVICES] = False 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): """Set attribute of device in deCONZ. Entity is used to resolve to a device path (e.g. '/lights/1'). Field is a string representing either a full path (e.g. '/lights/1/state') when entity is not specified, or a subpath (e.g. '/state') when used together with entity. Data is a json object with what data you want to alter e.g. data={'on': true}. { "field": "/lights/1/state", "data": {"on": true} } See Dresden Elektroniks REST API documentation for details: http://dresden-elektronik.github.io/deconz-rest-doc/rest/ """ gateway = get_master_gateway(hass) if CONF_BRIDGE_ID in data: gateway = hass.data[DOMAIN][normalize_bridge_id(data[CONF_BRIDGE_ID])] field = data.get(SERVICE_FIELD, "") entity_id = data.get(SERVICE_ENTITY) data = data[SERVICE_DATA] if entity_id: try: field = gateway.deconz_ids[entity_id] + field except KeyError: LOGGER.error("Could not find the entity %s", entity_id) return await gateway.api.request("put", field, json=data) async def async_refresh_devices_service(hass, data): """Refresh available devices from deCONZ.""" gateway = get_master_gateway(hass) if CONF_BRIDGE_ID in data: gateway = hass.data[DOMAIN][normalize_bridge_id(data[CONF_BRIDGE_ID])] gateway.ignore_state_updates = True await gateway.api.refresh_state() gateway.ignore_state_updates = False gateway.async_add_device_callback(NEW_GROUP, force=True) gateway.async_add_device_callback(NEW_LIGHT, force=True) gateway.async_add_device_callback(NEW_SCENE, force=True) gateway.async_add_device_callback(NEW_SENSOR, force=True) 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])] device_registry, entity_registry = await asyncio.gather( hass.helpers.device_registry.async_get_registry(), hass.helpers.entity_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 host entry gateway_host = device_registry.async_get_device( connections={(CONNECTION_NETWORK_MAC, gateway.api.config.mac)}, identifiers=set(), ) if gateway_host.id in devices_to_be_removed: devices_to_be_removed.remove(gateway_host.id) # Don't remove the Gateway service entry gateway_service = device_registry.async_get_device( identifiers={(DOMAIN, gateway.api.config.bridgeid)}, connections=set() ) if gateway_service.id in devices_to_be_removed: devices_to_be_removed.remove(gateway_service.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, include_disabled_entities=True ) ) == 0 ): device_registry.async_remove_device(device_id)