-
-
Notifications
You must be signed in to change notification settings - Fork 28.6k
/
services.py
223 lines (178 loc) · 6.99 KB
/
services.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
"""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)