Home Assistant Unofficial Reference 2024.12.1
services.py
Go to the documentation of this file.
1 """deCONZ services."""
2 
3 from pydeconz.utils import normalize_bridge_id
4 import voluptuous as vol
5 
6 from homeassistant.core import HomeAssistant, ServiceCall, callback
7 from homeassistant.helpers import (
8  config_validation as cv,
9  device_registry as dr,
10  entity_registry as er,
11 )
12 from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
13 from homeassistant.util.read_only_dict import ReadOnlyDict
14 
15 from .config_flow import get_master_hub
16 from .const import CONF_BRIDGE_ID, DOMAIN, LOGGER
17 from .hub import DeconzHub
18 
19 DECONZ_SERVICES = "deconz_services"
20 
21 SERVICE_FIELD = "field"
22 SERVICE_ENTITY = "entity"
23 SERVICE_DATA = "data"
24 
25 SERVICE_CONFIGURE_DEVICE = "configure"
26 SERVICE_CONFIGURE_DEVICE_SCHEMA = vol.All(
27  vol.Schema(
28  {
29  vol.Optional(SERVICE_ENTITY): cv.entity_id,
30  vol.Optional(SERVICE_FIELD): cv.matches_regex("/.*"),
31  vol.Required(SERVICE_DATA): dict,
32  vol.Optional(CONF_BRIDGE_ID): str,
33  }
34  ),
35  cv.has_at_least_one_key(SERVICE_ENTITY, SERVICE_FIELD),
36 )
37 
38 SERVICE_DEVICE_REFRESH = "device_refresh"
39 SERVICE_REMOVE_ORPHANED_ENTRIES = "remove_orphaned_entries"
40 SELECT_GATEWAY_SCHEMA = vol.All(vol.Schema({vol.Optional(CONF_BRIDGE_ID): str}))
41 
42 SUPPORTED_SERVICES = (
43  SERVICE_CONFIGURE_DEVICE,
44  SERVICE_DEVICE_REFRESH,
45  SERVICE_REMOVE_ORPHANED_ENTRIES,
46 )
47 
48 SERVICE_TO_SCHEMA = {
49  SERVICE_CONFIGURE_DEVICE: SERVICE_CONFIGURE_DEVICE_SCHEMA,
50  SERVICE_DEVICE_REFRESH: SELECT_GATEWAY_SCHEMA,
51  SERVICE_REMOVE_ORPHANED_ENTRIES: SELECT_GATEWAY_SCHEMA,
52 }
53 
54 
55 @callback
56 def async_setup_services(hass: HomeAssistant) -> None:
57  """Set up services for deCONZ integration."""
58 
59  async def async_call_deconz_service(service_call: ServiceCall) -> None:
60  """Call correct deCONZ service."""
61  service = service_call.service
62  service_data = service_call.data
63 
64  if CONF_BRIDGE_ID in service_data:
65  found_hub = False
66  bridge_id = normalize_bridge_id(service_data[CONF_BRIDGE_ID])
67 
68  for possible_hub in hass.data[DOMAIN].values():
69  if possible_hub.bridgeid == bridge_id:
70  hub = possible_hub
71  found_hub = True
72  break
73 
74  if not found_hub:
75  LOGGER.error("Could not find the gateway %s", bridge_id)
76  return
77  else:
78  try:
79  hub = get_master_hub(hass)
80  except ValueError:
81  LOGGER.error("No master gateway available")
82  return
83 
84  if service == SERVICE_CONFIGURE_DEVICE:
85  await async_configure_service(hub, service_data)
86 
87  elif service == SERVICE_DEVICE_REFRESH:
89 
90  elif service == SERVICE_REMOVE_ORPHANED_ENTRIES:
92 
93  for service in SUPPORTED_SERVICES:
94  hass.services.async_register(
95  DOMAIN,
96  service,
97  async_call_deconz_service,
98  schema=SERVICE_TO_SCHEMA[service],
99  )
100 
101 
102 async def async_configure_service(hub: DeconzHub, data: ReadOnlyDict) -> None:
103  """Set attribute of device in deCONZ.
104 
105  Entity is used to resolve to a device path (e.g. '/lights/1').
106  Field is a string representing either a full path
107  (e.g. '/lights/1/state') when entity is not specified, or a
108  subpath (e.g. '/state') when used together with entity.
109  Data is a json object with what data you want to alter
110  e.g. data={'on': true}.
111  {
112  "field": "/lights/1/state",
113  "data": {"on": true}
114  }
115  See Dresden Elektroniks REST API documentation for details:
116  http://dresden-elektronik.github.io/deconz-rest-doc/rest/
117  """
118  field = data.get(SERVICE_FIELD, "")
119  entity_id = data.get(SERVICE_ENTITY)
120  data = data[SERVICE_DATA]
121 
122  if entity_id:
123  try:
124  field = hub.deconz_ids[entity_id] + field
125  except KeyError:
126  LOGGER.error("Could not find the entity %s", entity_id)
127  return
128 
129  await hub.api.request("put", field, json=data)
130 
131 
132 async def async_refresh_devices_service(hub: DeconzHub) -> None:
133  """Refresh available devices from deCONZ."""
134  hub.ignore_state_updates = True
135  await hub.api.refresh_state()
136  hub.load_ignored_devices()
137  hub.ignore_state_updates = False
138 
139 
140 async def async_remove_orphaned_entries_service(hub: DeconzHub) -> None:
141  """Remove orphaned deCONZ entries from device and entity registries."""
142  device_registry = dr.async_get(hub.hass)
143  entity_registry = er.async_get(hub.hass)
144 
145  entity_entries = er.async_entries_for_config_entry(
146  entity_registry, hub.config_entry.entry_id
147  )
148 
149  entities_to_be_removed = []
150  devices_to_be_removed = [
151  entry.id
152  for entry in device_registry.devices.get_devices_for_config_entry_id(
153  hub.config_entry.entry_id
154  )
155  ]
156 
157  # Don't remove the Gateway host entry
158  if hub.api.config.mac:
159  hub_host = device_registry.async_get_device(
160  connections={(CONNECTION_NETWORK_MAC, hub.api.config.mac)},
161  )
162  if hub_host and hub_host.id in devices_to_be_removed:
163  devices_to_be_removed.remove(hub_host.id)
164 
165  # Don't remove the Gateway service entry
166  hub_service = device_registry.async_get_device(
167  identifiers={(DOMAIN, hub.api.config.bridge_id)}
168  )
169  if hub_service and hub_service.id in devices_to_be_removed:
170  devices_to_be_removed.remove(hub_service.id)
171 
172  # Don't remove devices belonging to available events
173  for event in hub.events:
174  if event.device_id in devices_to_be_removed:
175  devices_to_be_removed.remove(event.device_id)
176 
177  for entry in entity_entries:
178  # Don't remove available entities
179  if entry.unique_id in hub.entities[entry.domain]:
180  # Don't remove devices with available entities
181  if entry.device_id in devices_to_be_removed:
182  devices_to_be_removed.remove(entry.device_id)
183  continue
184  # Remove entities that are not available
185  entities_to_be_removed.append(entry.entity_id)
186 
187  # Remove unavailable entities
188  for entity_id in entities_to_be_removed:
189  entity_registry.async_remove(entity_id)
190 
191  # Remove devices that don't belong to any entity
192  for device_id in devices_to_be_removed:
193  if (
194  len(
195  er.async_entries_for_device(
196  entity_registry, device_id, include_disabled_entities=True
197  )
198  )
199  == 0
200  ):
201  device_registry.async_remove_device(device_id)
DeconzHub get_master_hub(HomeAssistant hass)
Definition: config_flow.py:55
None async_setup_services(HomeAssistant hass)
Definition: services.py:56
None async_configure_service(DeconzHub hub, ReadOnlyDict data)
Definition: services.py:102
None async_refresh_devices_service(DeconzHub hub)
Definition: services.py:132
None async_remove_orphaned_entries_service(DeconzHub hub)
Definition: services.py:140