plugwise 0.36.2__py3-none-any.whl → 0.37.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plugwise/__init__.py +187 -713
- plugwise/data.py +263 -0
- plugwise/helper.py +141 -381
- plugwise/legacy/data.py +124 -0
- plugwise/legacy/helper.py +775 -0
- plugwise/legacy/smile.py +272 -0
- plugwise/smile.py +426 -0
- plugwise/util.py +35 -1
- {plugwise-0.36.2.dist-info → plugwise-0.37.0.dist-info}/METADATA +2 -2
- plugwise-0.37.0.dist-info/RECORD +16 -0
- {plugwise-0.36.2.dist-info → plugwise-0.37.0.dist-info}/WHEEL +1 -1
- plugwise-0.36.2.dist-info/RECORD +0 -11
- {plugwise-0.36.2.dist-info → plugwise-0.37.0.dist-info}/LICENSE +0 -0
- {plugwise-0.36.2.dist-info → plugwise-0.37.0.dist-info}/top_level.txt +0 -0
plugwise/helper.py
CHANGED
@@ -8,32 +8,19 @@ import asyncio
|
|
8
8
|
import datetime as dt
|
9
9
|
from typing import cast
|
10
10
|
|
11
|
-
|
12
|
-
from aiohttp import BasicAuth, ClientError, ClientResponse, ClientSession, ClientTimeout
|
13
|
-
|
14
|
-
# Time related
|
15
|
-
from dateutil import tz
|
16
|
-
from dateutil.parser import parse
|
17
|
-
from defusedxml import ElementTree as etree
|
18
|
-
from munch import Munch
|
19
|
-
import semver
|
20
|
-
|
21
|
-
from .constants import (
|
11
|
+
from plugwise.constants import (
|
22
12
|
ACTIVE_ACTUATORS,
|
23
13
|
ACTUATOR_CLASSES,
|
24
14
|
ADAM,
|
25
15
|
ANNA,
|
26
|
-
APPLIANCES,
|
27
16
|
ATTR_NAME,
|
28
17
|
ATTR_UNIT_OF_MEASUREMENT,
|
29
18
|
BINARY_SENSORS,
|
30
19
|
DATA,
|
31
20
|
DEVICE_MEASUREMENTS,
|
32
21
|
DHW_SETPOINT,
|
33
|
-
|
22
|
+
DOMAIN_OBJECTS,
|
34
23
|
ENERGY_WATT_HOUR,
|
35
|
-
FAKE_APPL,
|
36
|
-
FAKE_LOC,
|
37
24
|
HEATER_CENTRAL_MEASUREMENTS,
|
38
25
|
LIMITS,
|
39
26
|
LOCATIONS,
|
@@ -41,9 +28,7 @@ from .constants import (
|
|
41
28
|
NONE,
|
42
29
|
OBSOLETE_MEASUREMENTS,
|
43
30
|
OFF,
|
44
|
-
P1_LEGACY_MEASUREMENTS,
|
45
31
|
P1_MEASUREMENTS,
|
46
|
-
POWER_WATT,
|
47
32
|
SENSORS,
|
48
33
|
SPECIAL_PLUG_TYPES,
|
49
34
|
SPECIALS,
|
@@ -67,43 +52,27 @@ from .constants import (
|
|
67
52
|
ThermoLoc,
|
68
53
|
ToggleNameType,
|
69
54
|
)
|
70
|
-
from .exceptions import (
|
55
|
+
from plugwise.exceptions import (
|
71
56
|
ConnectionFailedError,
|
72
57
|
InvalidAuthentication,
|
73
58
|
InvalidXMLError,
|
74
59
|
ResponseError,
|
75
60
|
)
|
76
|
-
from .util import
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
return model
|
83
|
-
|
84
|
-
return name
|
85
|
-
|
86
|
-
|
87
|
-
def etree_to_dict(element: etree) -> dict[str, str]:
|
88
|
-
"""Helper-function translating xml Element to dict."""
|
89
|
-
node: dict[str, str] = {}
|
90
|
-
if element is not None:
|
91
|
-
node.update(element.items())
|
92
|
-
|
93
|
-
return node
|
94
|
-
|
61
|
+
from plugwise.util import (
|
62
|
+
check_model,
|
63
|
+
escape_illegal_xml_characters,
|
64
|
+
format_measure,
|
65
|
+
power_data_local_format,
|
66
|
+
)
|
95
67
|
|
96
|
-
|
97
|
-
|
98
|
-
) -> float | int:
|
99
|
-
"""Format power data."""
|
100
|
-
# Special formatting of P1_MEASUREMENT POWER_WATT values, do not move to util-format_measure() function!
|
101
|
-
if all(item in key_string for item in ("electricity", "cumulative")):
|
102
|
-
return format_measure(val, ENERGY_KILO_WATT_HOUR)
|
103
|
-
if (attrs_uom := getattr(attrs, ATTR_UNIT_OF_MEASUREMENT)) == POWER_WATT:
|
104
|
-
return int(round(float(val)))
|
68
|
+
# This way of importing aiohttp is because of patch/mocking in testing (aiohttp timeouts)
|
69
|
+
from aiohttp import BasicAuth, ClientError, ClientResponse, ClientSession, ClientTimeout
|
105
70
|
|
106
|
-
|
71
|
+
# Time related
|
72
|
+
from dateutil import tz
|
73
|
+
from dateutil.parser import parse
|
74
|
+
from defusedxml import ElementTree as etree
|
75
|
+
from munch import Munch
|
107
76
|
|
108
77
|
|
109
78
|
class SmileComm:
|
@@ -113,10 +82,10 @@ class SmileComm:
|
|
113
82
|
self,
|
114
83
|
host: str,
|
115
84
|
password: str,
|
85
|
+
websession: ClientSession | None,
|
116
86
|
username: str,
|
117
87
|
port: int,
|
118
88
|
timeout: float,
|
119
|
-
websession: ClientSession | None,
|
120
89
|
) -> None:
|
121
90
|
"""Set the constructor for this class."""
|
122
91
|
if not websession:
|
@@ -148,6 +117,7 @@ class SmileComm:
|
|
148
117
|
# Command accepted gives empty body with status 202
|
149
118
|
if resp.status == 202:
|
150
119
|
return
|
120
|
+
|
151
121
|
# Cornercase for stretch not responding with 202
|
152
122
|
if method == "put" and resp.status == 200:
|
153
123
|
return
|
@@ -164,9 +134,9 @@ class SmileComm:
|
|
164
134
|
try:
|
165
135
|
# Encode to ensure utf8 parsing
|
166
136
|
xml = etree.XML(escape_illegal_xml_characters(result).encode())
|
167
|
-
except etree.ParseError:
|
137
|
+
except etree.ParseError as exc:
|
168
138
|
LOGGER.warning("Smile returns invalid XML for %s", self._endpoint)
|
169
|
-
raise InvalidXMLError
|
139
|
+
raise InvalidXMLError from exc
|
170
140
|
|
171
141
|
return xml
|
172
142
|
|
@@ -202,15 +172,15 @@ class SmileComm:
|
|
202
172
|
)
|
203
173
|
except (
|
204
174
|
ClientError
|
205
|
-
) as
|
175
|
+
) as exc: # ClientError is an ancestor class of ServerTimeoutError
|
206
176
|
if retry < 1:
|
207
177
|
LOGGER.warning(
|
208
178
|
"Failed sending %s %s to Plugwise Smile, error: %s",
|
209
179
|
method,
|
210
180
|
command,
|
211
|
-
|
181
|
+
exc,
|
212
182
|
)
|
213
|
-
raise ConnectionFailedError
|
183
|
+
raise ConnectionFailedError from exc
|
214
184
|
return await self._request(command, retry - 1)
|
215
185
|
|
216
186
|
return await self._request_validate(resp, method)
|
@@ -225,33 +195,29 @@ class SmileHelper:
|
|
225
195
|
|
226
196
|
def __init__(self) -> None:
|
227
197
|
"""Set the constructor for this class."""
|
228
|
-
self._appliances: etree
|
229
198
|
self._cooling_activation_outdoor_temp: float
|
230
199
|
self._cooling_deactivation_threshold: float
|
231
|
-
self._cooling_present
|
200
|
+
self._cooling_present: bool
|
232
201
|
self._count: int
|
233
202
|
self._dhw_allowed_modes: list[str] = []
|
234
203
|
self._domain_objects: etree
|
235
|
-
self.
|
204
|
+
self._endpoint: str
|
205
|
+
self._elga: bool
|
236
206
|
self._gw_allowed_modes: list[str] = []
|
237
207
|
self._heater_id: str
|
238
208
|
self._home_location: str
|
239
|
-
self._is_thermostat
|
240
|
-
self._last_active: dict[str, str | None]
|
209
|
+
self._is_thermostat: bool
|
210
|
+
self._last_active: dict[str, str | None]
|
241
211
|
self._last_modified: dict[str, str] = {}
|
242
|
-
self._locations: etree
|
243
|
-
self._loc_data: dict[str, ThermoLoc] = {}
|
244
|
-
self._modules: etree
|
245
212
|
self._notifications: dict[str, dict[str, str]] = {}
|
246
|
-
self._on_off_device
|
247
|
-
self._opentherm_device
|
213
|
+
self._on_off_device: bool
|
214
|
+
self._opentherm_device: bool
|
248
215
|
self._outdoor_temp: float
|
249
216
|
self._reg_allowed_modes: list[str] = []
|
250
217
|
self._schedule_old_states: dict[str, dict[str, str]] = {}
|
251
218
|
self._smile_legacy = False
|
252
219
|
self._status: etree
|
253
|
-
self._stretch_v2
|
254
|
-
self._stretch_v3 = False
|
220
|
+
self._stretch_v2: bool
|
255
221
|
self._system: etree
|
256
222
|
self._thermo_locs: dict[str, ThermoLoc] = {}
|
257
223
|
###################################################################
|
@@ -270,17 +236,16 @@ class SmileHelper:
|
|
270
236
|
self._cooling_enabled = False
|
271
237
|
|
272
238
|
self.device_items: int = 0
|
273
|
-
self.device_list: list[str]
|
274
239
|
self.gateway_id: str
|
275
240
|
self.gw_data: GatewayData = {}
|
276
241
|
self.gw_devices: dict[str, DeviceData] = {}
|
242
|
+
self.loc_data: dict[str, ThermoLoc]
|
277
243
|
self.smile_fw_version: str | None = None
|
278
244
|
self.smile_hw_version: str | None = None
|
279
245
|
self.smile_mac_address: str | None = None
|
280
246
|
self.smile_model: str
|
281
247
|
self.smile_name: str
|
282
248
|
self.smile_type: str
|
283
|
-
self.smile_version: tuple[str, semver.version.Version]
|
284
249
|
self.smile_zigbee_mac_address: str | None = None
|
285
250
|
self.therms_with_offset_func: list[str] = []
|
286
251
|
|
@@ -291,34 +256,14 @@ class SmileHelper:
|
|
291
256
|
def _all_locations(self) -> None:
|
292
257
|
"""Collect all locations."""
|
293
258
|
loc = Munch()
|
294
|
-
|
295
|
-
locations = self._locations.findall("./location")
|
296
|
-
# Legacy Anna without outdoor_temp and Stretches have no locations, create fake location-data
|
297
|
-
if not locations and self._smile_legacy:
|
298
|
-
self._home_location = FAKE_LOC
|
299
|
-
self._loc_data[FAKE_LOC] = {"name": "Home"}
|
300
|
-
return
|
301
|
-
|
259
|
+
locations = self._domain_objects.findall("./location")
|
302
260
|
for location in locations:
|
303
261
|
loc.name = location.find("name").text
|
304
262
|
loc.loc_id = location.attrib["id"]
|
305
|
-
# Filter the valid single location for P1 legacy: services not empty
|
306
|
-
locator = "./services"
|
307
|
-
if (
|
308
|
-
self._smile_legacy
|
309
|
-
and self.smile_type == "power"
|
310
|
-
and len(location.find(locator)) == 0
|
311
|
-
):
|
312
|
-
continue
|
313
|
-
|
314
263
|
if loc.name == "Home":
|
315
264
|
self._home_location = loc.loc_id
|
316
|
-
# Replace location-name for P1 legacy, can contain privacy-related info
|
317
|
-
if self._smile_legacy and self.smile_type == "power":
|
318
|
-
loc.name = "Home"
|
319
|
-
self._home_location = loc.loc_id
|
320
265
|
|
321
|
-
self.
|
266
|
+
self.loc_data[loc.loc_id] = {"name": loc.name}
|
322
267
|
|
323
268
|
def _get_module_data(
|
324
269
|
self, appliance: etree, locator: str, mod_type: str
|
@@ -338,9 +283,9 @@ class SmileHelper:
|
|
338
283
|
}
|
339
284
|
if (appl_search := appliance.find(locator)) is not None:
|
340
285
|
link_id = appl_search.attrib["id"]
|
341
|
-
loc = f".//{mod_type}[@id='{link_id}']...."
|
286
|
+
loc = f".//services/{mod_type}[@id='{link_id}']...."
|
342
287
|
# Not possible to walrus for some reason...
|
343
|
-
module = self.
|
288
|
+
module = self._domain_objects.find(loc)
|
344
289
|
if module is not None: # pylint: disable=consider-using-assignment-expr
|
345
290
|
model_data["contents"] = True
|
346
291
|
if (vendor_name := module.find("vendor_name").text) is not None:
|
@@ -354,12 +299,6 @@ class SmileHelper:
|
|
354
299
|
if (zb_node := module.find("./protocols/zig_bee_node")) is not None:
|
355
300
|
model_data["zigbee_mac_address"] = zb_node.find("mac_address").text
|
356
301
|
model_data["reachable"] = zb_node.find("reachable").text == "true"
|
357
|
-
# Stretches
|
358
|
-
if (router := module.find("./protocols/network_router")) is not None:
|
359
|
-
model_data["zigbee_mac_address"] = router.find("mac_address").text
|
360
|
-
# Also look for the Circle+/Stealth M+
|
361
|
-
if (coord := module.find("./protocols/network_coordinator")) is not None:
|
362
|
-
model_data["zigbee_mac_address"] = coord.find("mac_address").text
|
363
302
|
|
364
303
|
return model_data
|
365
304
|
|
@@ -368,24 +307,14 @@ class SmileHelper:
|
|
368
307
|
|
369
308
|
Collect energy device info (Circle, Plug, Stealth): firmware, model and vendor name.
|
370
309
|
"""
|
371
|
-
if self.smile_type
|
372
|
-
locator = "./
|
373
|
-
if not self._smile_legacy:
|
374
|
-
locator = "./logs/point_log/electricity_point_meter"
|
310
|
+
if self.smile_type == "power":
|
311
|
+
locator = "./logs/point_log/electricity_point_meter"
|
375
312
|
mod_type = "electricity_point_meter"
|
376
|
-
|
377
313
|
module_data = self._get_module_data(appliance, locator, mod_type)
|
378
|
-
# Filter appliance without zigbee_mac, it's an orphaned device
|
379
314
|
appl.zigbee_mac = module_data["zigbee_mac_address"]
|
380
|
-
if appl.zigbee_mac is None and self.smile_type != "power":
|
381
|
-
return None
|
382
|
-
|
383
315
|
appl.hardware = module_data["hardware_version"]
|
384
316
|
appl.model = module_data["vendor_model"]
|
385
317
|
appl.vendor_name = module_data["vendor_name"]
|
386
|
-
if appl.hardware is not None:
|
387
|
-
hw_version = appl.hardware.replace("-", "")
|
388
|
-
appl.model = version_to_model(hw_version)
|
389
318
|
appl.firmware = module_data["firmware_version"]
|
390
319
|
|
391
320
|
return appl
|
@@ -422,7 +351,7 @@ class SmileHelper:
|
|
422
351
|
|
423
352
|
# Adam: look for the ZigBee MAC address of the Smile
|
424
353
|
if self.smile(ADAM) and (
|
425
|
-
(found := self.
|
354
|
+
(found := self._domain_objects.find(".//protocols/zig_bee_coordinator")) is not None
|
426
355
|
):
|
427
356
|
appl.zigbee_mac = found.find("mac_address").text
|
428
357
|
|
@@ -463,7 +392,7 @@ class SmileHelper:
|
|
463
392
|
if appl.pwclass == "heater_central":
|
464
393
|
# Remove heater_central when no active device present
|
465
394
|
if not self._opentherm_device and not self._on_off_device:
|
466
|
-
return None
|
395
|
+
return None # pragma: no cover
|
467
396
|
|
468
397
|
# Find the valid heater_central
|
469
398
|
self._heater_id = self._check_heater_central()
|
@@ -504,7 +433,7 @@ class SmileHelper:
|
|
504
433
|
|
505
434
|
return appl
|
506
435
|
|
507
|
-
# Collect info from
|
436
|
+
# Collect info from power-related devices (Plug, Aqara Smart Plug)
|
508
437
|
appl = self._energy_device_info_finder(appliance, appl)
|
509
438
|
|
510
439
|
return appl
|
@@ -518,7 +447,7 @@ class SmileHelper:
|
|
518
447
|
locator = "./appliance[type='heater_central']"
|
519
448
|
hc_count = 0
|
520
449
|
hc_list: list[dict[str, bool]] = []
|
521
|
-
for heater_central in self.
|
450
|
+
for heater_central in self._domain_objects.findall(locator):
|
522
451
|
hc_count += 1
|
523
452
|
hc_id: str = heater_central.attrib["id"]
|
524
453
|
has_actuators: bool = (
|
@@ -539,17 +468,15 @@ class SmileHelper:
|
|
539
468
|
|
540
469
|
def _p1_smartmeter_info_finder(self, appl: Munch) -> None:
|
541
470
|
"""Collect P1 DSMR Smartmeter info."""
|
542
|
-
loc_id = next(iter(self.
|
471
|
+
loc_id = next(iter(self.loc_data.keys()))
|
543
472
|
appl.dev_id = self.gateway_id
|
544
473
|
appl.location = loc_id
|
545
|
-
if self._smile_legacy:
|
546
|
-
appl.dev_id = loc_id
|
547
474
|
appl.mac = None
|
548
475
|
appl.model = self.smile_model
|
549
476
|
appl.name = "P1"
|
550
477
|
appl.pwclass = "smartmeter"
|
551
478
|
appl.zigbee_mac = None
|
552
|
-
location = self.
|
479
|
+
location = self._domain_objects.find(f'./location[@id="{loc_id}"]')
|
553
480
|
appl = self._energy_device_info_finder(location, appl)
|
554
481
|
|
555
482
|
self.gw_devices[appl.dev_id] = {"dev_class": appl.pwclass}
|
@@ -570,46 +497,12 @@ class SmileHelper:
|
|
570
497
|
self.gw_devices[appl.dev_id][p1_key] = value
|
571
498
|
self._count += 1
|
572
499
|
|
573
|
-
def _create_legacy_gateway(self) -> None:
|
574
|
-
"""Create the (missing) gateway devices for legacy Anna, P1 and Stretch.
|
575
|
-
|
576
|
-
Use the home_location or FAKE_APPL as device id.
|
577
|
-
"""
|
578
|
-
self.gateway_id = self._home_location
|
579
|
-
if self.smile_type == "power":
|
580
|
-
self.gateway_id = FAKE_APPL
|
581
|
-
|
582
|
-
self.gw_devices[self.gateway_id] = {"dev_class": "gateway"}
|
583
|
-
self._count += 1
|
584
|
-
for key, value in {
|
585
|
-
"firmware": self.smile_fw_version,
|
586
|
-
"location": self._home_location,
|
587
|
-
"mac_address": self.smile_mac_address,
|
588
|
-
"model": self.smile_model,
|
589
|
-
"name": self.smile_name,
|
590
|
-
"zigbee_mac_address": self.smile_zigbee_mac_address,
|
591
|
-
"vendor": "Plugwise",
|
592
|
-
}.items():
|
593
|
-
if value is not None:
|
594
|
-
gw_key = cast(ApplianceType, key)
|
595
|
-
self.gw_devices[self.gateway_id][gw_key] = value
|
596
|
-
self._count += 1
|
597
|
-
|
598
500
|
def _all_appliances(self) -> None:
|
599
501
|
"""Collect all appliances with relevant info."""
|
600
502
|
self._count = 0
|
601
503
|
self._all_locations()
|
602
504
|
|
603
|
-
|
604
|
-
self._create_legacy_gateway()
|
605
|
-
# For legacy P1 collect the connected SmartMeter info
|
606
|
-
if self.smile_type == "power":
|
607
|
-
appl = Munch()
|
608
|
-
self._p1_smartmeter_info_finder(appl)
|
609
|
-
# Legacy P1 has no more devices
|
610
|
-
return
|
611
|
-
|
612
|
-
for appliance in self._appliances.findall("./appliance"):
|
505
|
+
for appliance in self._domain_objects.findall("./appliance"):
|
613
506
|
appl = Munch()
|
614
507
|
appl.pwclass = appliance.find("type").text
|
615
508
|
# Skip thermostats that have this key, should be an orphaned device (Core #81712)
|
@@ -622,11 +515,9 @@ class SmileHelper:
|
|
622
515
|
appl.location = None
|
623
516
|
if (appl_loc := appliance.find("location")) is not None:
|
624
517
|
appl.location = appl_loc.attrib["id"]
|
625
|
-
#
|
626
|
-
#
|
627
|
-
elif
|
628
|
-
self._smile_legacy and self.smile_type == "thermostat"
|
629
|
-
) or appl.pwclass not in THERMOSTAT_CLASSES:
|
518
|
+
# Don't assign the _home_location to thermostat-devices without a location,
|
519
|
+
# they are not active
|
520
|
+
elif appl.pwclass not in THERMOSTAT_CLASSES:
|
630
521
|
appl.location = self._home_location
|
631
522
|
|
632
523
|
appl.dev_id = appliance.attrib["id"]
|
@@ -639,7 +530,7 @@ class SmileHelper:
|
|
639
530
|
appl.vendor_name = None
|
640
531
|
|
641
532
|
# Determine class for this appliance
|
642
|
-
# Skip on heater_central when no active device present
|
533
|
+
# Skip on heater_central when no active device present
|
643
534
|
if not (appl := self._appliance_info_finder(appliance, appl)):
|
644
535
|
continue
|
645
536
|
|
@@ -652,12 +543,8 @@ class SmileHelper:
|
|
652
543
|
if appl.pwclass == "gateway" and self.smile_type == "power":
|
653
544
|
appl.dev_id = appl.location
|
654
545
|
|
655
|
-
# Don't show orphaned
|
656
|
-
if
|
657
|
-
not self._smile_legacy
|
658
|
-
and appl.pwclass in THERMOSTAT_CLASSES
|
659
|
-
and appl.location is None
|
660
|
-
):
|
546
|
+
# Don't show orphaned thermostat-types or the OpenTherm Gateway.
|
547
|
+
if appl.pwclass in THERMOSTAT_CLASSES and appl.location is None:
|
661
548
|
continue
|
662
549
|
|
663
550
|
self.gw_devices[appl.dev_id] = {"dev_class": appl.pwclass}
|
@@ -677,7 +564,7 @@ class SmileHelper:
|
|
677
564
|
self.gw_devices[appl.dev_id][appl_key] = value
|
678
565
|
self._count += 1
|
679
566
|
|
680
|
-
# For
|
567
|
+
# For P1 collect the connected SmartMeter info
|
681
568
|
if self.smile_type == "power":
|
682
569
|
self._p1_smartmeter_info_finder(appl)
|
683
570
|
# P1: for gateway and smartmeter switch device_id - part 2
|
@@ -697,22 +584,6 @@ class SmileHelper:
|
|
697
584
|
add_to_front = {dev_id: tmp_device}
|
698
585
|
self.gw_devices = {**add_to_front, **cleared_dict}
|
699
586
|
|
700
|
-
def _match_locations(self) -> dict[str, ThermoLoc]:
|
701
|
-
"""Helper-function for _scan_thermostats().
|
702
|
-
|
703
|
-
Match appliances with locations.
|
704
|
-
"""
|
705
|
-
matched_locations: dict[str, ThermoLoc] = {}
|
706
|
-
for location_id, location_details in self._loc_data.items():
|
707
|
-
for appliance_details in self.gw_devices.values():
|
708
|
-
if appliance_details["location"] == location_id:
|
709
|
-
location_details.update(
|
710
|
-
{"master": None, "master_prio": 0, "slaves": set()}
|
711
|
-
)
|
712
|
-
matched_locations[location_id] = location_details
|
713
|
-
|
714
|
-
return matched_locations
|
715
|
-
|
716
587
|
def _control_state(self, loc_id: str) -> str | bool:
|
717
588
|
"""Helper-function for _device_data_adam().
|
718
589
|
|
@@ -728,28 +599,11 @@ class SmileHelper:
|
|
728
599
|
|
729
600
|
return False
|
730
601
|
|
731
|
-
def _presets_legacy(self) -> dict[str, list[float]]:
|
732
|
-
"""Helper-function for presets() - collect Presets for a legacy Anna."""
|
733
|
-
presets: dict[str, list[float]] = {}
|
734
|
-
for directive in self._domain_objects.findall("rule/directives/when/then"):
|
735
|
-
if directive is not None and directive.get("icon") is not None:
|
736
|
-
# Ensure list of heating_setpoint, cooling_setpoint
|
737
|
-
presets[directive.attrib["icon"]] = [
|
738
|
-
float(directive.attrib["temperature"]),
|
739
|
-
0,
|
740
|
-
]
|
741
|
-
|
742
|
-
return presets
|
743
|
-
|
744
602
|
def _presets(self, loc_id: str) -> dict[str, list[float]]:
|
745
603
|
"""Collect Presets for a Thermostat based on location_id."""
|
746
604
|
presets: dict[str, list[float]] = {}
|
747
605
|
tag_1 = "zone_setpoint_and_state_based_on_preset"
|
748
606
|
tag_2 = "Thermostat presets"
|
749
|
-
|
750
|
-
if self._smile_legacy:
|
751
|
-
return self._presets_legacy()
|
752
|
-
|
753
607
|
if not (rule_ids := self._rule_ids_by_tag(tag_1, loc_id)):
|
754
608
|
if not (rule_ids := self._rule_ids_by_name(tag_2, loc_id)):
|
755
609
|
return presets # pragma: no cover
|
@@ -767,35 +621,38 @@ class SmileHelper:
|
|
767
621
|
|
768
622
|
return presets
|
769
623
|
|
770
|
-
def _rule_ids_by_name(self, name: str, loc_id: str) -> dict[str, str]:
|
624
|
+
def _rule_ids_by_name(self, name: str, loc_id: str) -> dict[str, dict[str, str]]:
|
771
625
|
"""Helper-function for _presets().
|
772
626
|
|
773
627
|
Obtain the rule_id from the given name and and provide the location_id, when present.
|
774
628
|
"""
|
775
|
-
schedule_ids: dict[str, str] = {}
|
629
|
+
schedule_ids: dict[str, dict[str, str]] = {}
|
776
630
|
locator = f'./contexts/context/zone/location[@id="{loc_id}"]'
|
777
631
|
for rule in self._domain_objects.findall(f'./rule[name="{name}"]'):
|
632
|
+
active = rule.find("active").text
|
778
633
|
if rule.find(locator) is not None:
|
779
|
-
schedule_ids[rule.attrib["id"]] = loc_id
|
634
|
+
schedule_ids[rule.attrib["id"]] = {"location": loc_id, "name": name, "active": active}
|
780
635
|
else:
|
781
|
-
schedule_ids[rule.attrib["id"]] = NONE
|
636
|
+
schedule_ids[rule.attrib["id"]] = {"location": NONE, "name": name, "active": active}
|
782
637
|
|
783
638
|
return schedule_ids
|
784
639
|
|
785
|
-
def _rule_ids_by_tag(self, tag: str, loc_id: str) -> dict[str, str]:
|
640
|
+
def _rule_ids_by_tag(self, tag: str, loc_id: str) -> dict[str, dict[str, str]]:
|
786
641
|
"""Helper-function for _presets(), _schedules() and _last_active_schedule().
|
787
642
|
|
788
643
|
Obtain the rule_id from the given template_tag and provide the location_id, when present.
|
789
644
|
"""
|
790
|
-
schedule_ids: dict[str, str] = {}
|
645
|
+
schedule_ids: dict[str, dict[str, str]] = {}
|
791
646
|
locator1 = f'./template[@tag="{tag}"]'
|
792
647
|
locator2 = f'./contexts/context/zone/location[@id="{loc_id}"]'
|
793
648
|
for rule in self._domain_objects.findall("./rule"):
|
794
649
|
if rule.find(locator1) is not None:
|
650
|
+
name = rule.find("name").text
|
651
|
+
active = rule.find("active").text
|
795
652
|
if rule.find(locator2) is not None:
|
796
|
-
schedule_ids[rule.attrib["id"]] = loc_id
|
653
|
+
schedule_ids[rule.attrib["id"]] = {"location": loc_id, "name": name, "active": active}
|
797
654
|
else:
|
798
|
-
schedule_ids[rule.attrib["id"]] = NONE
|
655
|
+
schedule_ids[rule.attrib["id"]] = {"location": NONE, "name": name, "active": active}
|
799
656
|
|
800
657
|
return schedule_ids
|
801
658
|
|
@@ -809,9 +666,6 @@ class SmileHelper:
|
|
809
666
|
for measurement, attrs in measurements.items():
|
810
667
|
p_locator = f'.//logs/point_log[type="{measurement}"]/period/measurement'
|
811
668
|
if (appl_p_loc := appliance.find(p_locator)) is not None:
|
812
|
-
if self._smile_legacy and measurement == "domestic_hot_water_state":
|
813
|
-
continue
|
814
|
-
|
815
669
|
# Skip known obsolete measurements
|
816
670
|
updated_date_locator = (
|
817
671
|
f'.//logs/point_log[type="{measurement}"]/updated_date'
|
@@ -882,10 +736,10 @@ class SmileHelper:
|
|
882
736
|
# Don't count the above top-level dicts, only the remaining single items
|
883
737
|
self._count += len(data) - 3
|
884
738
|
|
885
|
-
def
|
739
|
+
def _wireless_availability(self, appliance: etree, data: DeviceData) -> None:
|
886
740
|
"""Helper-function for _get_measurement_data().
|
887
741
|
|
888
|
-
Collect the
|
742
|
+
Collect the availability-status for wireless connected devices.
|
889
743
|
"""
|
890
744
|
if self.smile(ADAM):
|
891
745
|
# Collect for Plugs
|
@@ -905,7 +759,7 @@ class SmileHelper:
|
|
905
759
|
def _get_appliances_with_offset_functionality(self) -> list[str]:
|
906
760
|
"""Helper-function collecting all appliance that have offset_functionality."""
|
907
761
|
therm_list: list[str] = []
|
908
|
-
offset_appls = self.
|
762
|
+
offset_appls = self._domain_objects.findall(
|
909
763
|
'.//actuator_functionalities/offset_functionality[type="temperature_offset"]/offset/../../..'
|
910
764
|
)
|
911
765
|
for item in offset_appls:
|
@@ -929,10 +783,6 @@ class SmileHelper:
|
|
929
783
|
functionality = "thermostat_functionality"
|
930
784
|
if item == "temperature_offset":
|
931
785
|
functionality = "offset_functionality"
|
932
|
-
# Don't support temperature_offset for legacy Anna
|
933
|
-
if self._smile_legacy:
|
934
|
-
continue
|
935
|
-
|
936
786
|
# When there is no updated_date-text, skip the actuator
|
937
787
|
updated_date_location = f'.//actuator_functionalities/{functionality}[type="{item}"]/updated_date'
|
938
788
|
if (
|
@@ -1048,19 +898,16 @@ class SmileHelper:
|
|
1048
898
|
Collect the appliance-data based on device id.
|
1049
899
|
"""
|
1050
900
|
data: DeviceData = {"binary_sensors": {}, "sensors": {}, "switches": {}}
|
1051
|
-
# Get P1 smartmeter data from LOCATIONS
|
901
|
+
# Get P1 smartmeter data from LOCATIONS
|
1052
902
|
device = self.gw_devices[dev_id]
|
1053
903
|
# !! DON'T CHANGE below two if-lines, will break stuff !!
|
1054
904
|
if self.smile_type == "power":
|
1055
905
|
if device["dev_class"] == "smartmeter":
|
1056
|
-
|
1057
|
-
data.update(self._power_data_from_location(device["location"]))
|
1058
|
-
else:
|
1059
|
-
data.update(self._power_data_from_modules())
|
906
|
+
data.update(self._power_data_from_location(device["location"]))
|
1060
907
|
|
1061
908
|
return data
|
1062
909
|
|
1063
|
-
# Get non-
|
910
|
+
# Get non-P1 data from APPLIANCES
|
1064
911
|
measurements = DEVICE_MEASUREMENTS
|
1065
912
|
if self._is_thermostat and dev_id == self._heater_id:
|
1066
913
|
measurements = HEATER_CENTRAL_MEASUREMENTS
|
@@ -1070,7 +917,7 @@ class SmileHelper:
|
|
1070
917
|
# Counting of this item is done in _appliance_measurements()
|
1071
918
|
|
1072
919
|
if (
|
1073
|
-
appliance := self.
|
920
|
+
appliance := self._domain_objects.find(f'./appliance[@id="{dev_id}"]')
|
1074
921
|
) is not None:
|
1075
922
|
self._appliance_measurements(appliance, data, measurements)
|
1076
923
|
self._get_lock_state(appliance, data)
|
@@ -1082,7 +929,7 @@ class SmileHelper:
|
|
1082
929
|
self._get_actuator_functionalities(appliance, device, data)
|
1083
930
|
|
1084
931
|
# Collect availability-status for wireless connected devices to Adam
|
1085
|
-
self.
|
932
|
+
self._wireless_availability(appliance, data)
|
1086
933
|
|
1087
934
|
if dev_id == self.gateway_id and self.smile(ADAM):
|
1088
935
|
self._get_regulation_mode(appliance, data)
|
@@ -1137,33 +984,6 @@ class SmileHelper:
|
|
1137
984
|
|
1138
985
|
return data
|
1139
986
|
|
1140
|
-
def _rank_thermostat(
|
1141
|
-
self,
|
1142
|
-
thermo_matching: dict[str, int],
|
1143
|
-
loc_id: str,
|
1144
|
-
appliance_id: str,
|
1145
|
-
appliance_details: DeviceData,
|
1146
|
-
) -> None:
|
1147
|
-
"""Helper-function for _scan_thermostats().
|
1148
|
-
|
1149
|
-
Rank the thermostat based on appliance_details: master or slave.
|
1150
|
-
"""
|
1151
|
-
appl_class = appliance_details["dev_class"]
|
1152
|
-
appl_d_loc = appliance_details["location"]
|
1153
|
-
if loc_id == appl_d_loc and appl_class in thermo_matching:
|
1154
|
-
# Pre-elect new master
|
1155
|
-
if thermo_matching[appl_class] > self._thermo_locs[loc_id]["master_prio"]:
|
1156
|
-
# Demote former master
|
1157
|
-
if (tl_master := self._thermo_locs[loc_id]["master"]) is not None:
|
1158
|
-
self._thermo_locs[loc_id]["slaves"].add(tl_master)
|
1159
|
-
|
1160
|
-
# Crown master
|
1161
|
-
self._thermo_locs[loc_id]["master_prio"] = thermo_matching[appl_class]
|
1162
|
-
self._thermo_locs[loc_id]["master"] = appliance_id
|
1163
|
-
|
1164
|
-
else:
|
1165
|
-
self._thermo_locs[loc_id]["slaves"].add(appliance_id)
|
1166
|
-
|
1167
987
|
def _scan_thermostats(self) -> None:
|
1168
988
|
"""Helper-function for smile.py: get_all_devices().
|
1169
989
|
|
@@ -1190,26 +1010,56 @@ class SmileHelper:
|
|
1190
1010
|
if "slaves" in tl_loc_id and dev_id in tl_loc_id["slaves"]:
|
1191
1011
|
device["dev_class"] = "thermo_sensor"
|
1192
1012
|
|
1193
|
-
def
|
1194
|
-
"""Helper-function for
|
1013
|
+
def _match_locations(self) -> dict[str, ThermoLoc]:
|
1014
|
+
"""Helper-function for _scan_thermostats().
|
1195
1015
|
|
1196
|
-
|
1016
|
+
Match appliances with locations.
|
1197
1017
|
"""
|
1198
|
-
|
1199
|
-
|
1018
|
+
matched_locations: dict[str, ThermoLoc] = {}
|
1019
|
+
for location_id, location_details in self.loc_data.items():
|
1020
|
+
for appliance_details in self.gw_devices.values():
|
1021
|
+
if appliance_details["location"] == location_id:
|
1022
|
+
location_details.update(
|
1023
|
+
{"master": None, "master_prio": 0, "slaves": set()}
|
1024
|
+
)
|
1025
|
+
matched_locations[location_id] = location_details
|
1026
|
+
|
1027
|
+
return matched_locations
|
1200
1028
|
|
1201
|
-
|
1029
|
+
def _rank_thermostat(
|
1030
|
+
self,
|
1031
|
+
thermo_matching: dict[str, int],
|
1032
|
+
loc_id: str,
|
1033
|
+
appliance_id: str,
|
1034
|
+
appliance_details: DeviceData,
|
1035
|
+
) -> None:
|
1036
|
+
"""Helper-function for _scan_thermostats().
|
1037
|
+
|
1038
|
+
Rank the thermostat based on appliance_details: master or slave.
|
1039
|
+
"""
|
1040
|
+
appl_class = appliance_details["dev_class"]
|
1041
|
+
appl_d_loc = appliance_details["location"]
|
1042
|
+
if loc_id == appl_d_loc and appl_class in thermo_matching:
|
1043
|
+
# Pre-elect new master
|
1044
|
+
if thermo_matching[appl_class] > self._thermo_locs[loc_id]["master_prio"]:
|
1045
|
+
# Demote former master
|
1046
|
+
if (tl_master := self._thermo_locs[loc_id]["master"]) is not None:
|
1047
|
+
self._thermo_locs[loc_id]["slaves"].add(tl_master)
|
1048
|
+
|
1049
|
+
# Crown master
|
1050
|
+
self._thermo_locs[loc_id]["master_prio"] = thermo_matching[appl_class]
|
1051
|
+
self._thermo_locs[loc_id]["master"] = appliance_id
|
1052
|
+
|
1053
|
+
else:
|
1054
|
+
self._thermo_locs[loc_id]["slaves"].add(appliance_id)
|
1202
1055
|
|
1203
1056
|
def _thermostat_uri(self, loc_id: str) -> str:
|
1204
1057
|
"""Helper-function for smile.py: set_temperature().
|
1205
1058
|
|
1206
1059
|
Determine the location-set_temperature uri - from LOCATIONS.
|
1207
1060
|
"""
|
1208
|
-
if self._smile_legacy:
|
1209
|
-
return self._thermostat_uri_legacy()
|
1210
|
-
|
1211
1061
|
locator = f'./location[@id="{loc_id}"]/actuator_functionalities/thermostat_functionality'
|
1212
|
-
thermostat_functionality_id = self.
|
1062
|
+
thermostat_functionality_id = self._domain_objects.find(locator).attrib["id"]
|
1213
1063
|
|
1214
1064
|
return f"{LOCATIONS};id={loc_id}/thermostat;id={thermostat_functionality_id}"
|
1215
1065
|
|
@@ -1229,8 +1079,8 @@ class SmileHelper:
|
|
1229
1079
|
group_name = group.find("name").text
|
1230
1080
|
group_type = group.find("type").text
|
1231
1081
|
group_appliances = group.findall("appliances/appliance")
|
1082
|
+
# Check if members are not orphaned
|
1232
1083
|
for item in group_appliances:
|
1233
|
-
# Check if members are not orphaned - stretch
|
1234
1084
|
if item.attrib["id"] in self.gw_devices:
|
1235
1085
|
members.append(item.attrib["id"])
|
1236
1086
|
|
@@ -1257,7 +1107,7 @@ class SmileHelper:
|
|
1257
1107
|
"""
|
1258
1108
|
loc_found: int = 0
|
1259
1109
|
open_valve_count: int = 0
|
1260
|
-
for appliance in self.
|
1110
|
+
for appliance in self._domain_objects.findall("./appliance"):
|
1261
1111
|
locator = './logs/point_log[type="valve_position"]/period/measurement'
|
1262
1112
|
if (appl_loc := appliance.find(locator)) is not None:
|
1263
1113
|
loc_found += 1
|
@@ -1301,7 +1151,6 @@ class SmileHelper:
|
|
1301
1151
|
"""Helper-function for _power_data_from_location() and _power_data_from_modules()."""
|
1302
1152
|
loc.found = True
|
1303
1153
|
# If locator not found look for P1 gas_consumed or phase data (without tariff)
|
1304
|
-
# or for P1 legacy electricity_point_meter or gas_*_meter data
|
1305
1154
|
if loc.logs.find(loc.locator) is None:
|
1306
1155
|
if "log" in loc.log_type and (
|
1307
1156
|
"gas" in loc.measurement or "phase" in loc.measurement
|
@@ -1317,25 +1166,9 @@ class SmileHelper:
|
|
1317
1166
|
if loc.logs.find(loc.locator) is None:
|
1318
1167
|
loc.found = False
|
1319
1168
|
return loc
|
1320
|
-
# P1 legacy point_meter has no tariff_indicator
|
1321
|
-
elif "meter" in loc.log_type and (
|
1322
|
-
"point" in loc.log_type or "gas" in loc.measurement
|
1323
|
-
):
|
1324
|
-
# Avoid double processing by skipping one peak-list option
|
1325
|
-
if loc.peak_select == "nl_offpeak":
|
1326
|
-
loc.found = False
|
1327
|
-
return loc
|
1328
|
-
|
1329
|
-
loc.locator = (
|
1330
|
-
f"./{loc.meas_list[0]}_{loc.log_type}/"
|
1331
|
-
f'measurement[@directionality="{loc.meas_list[1]}"]'
|
1332
|
-
)
|
1333
|
-
if loc.logs.find(loc.locator) is None:
|
1334
|
-
loc.found = False
|
1335
|
-
return loc
|
1336
1169
|
else:
|
1337
|
-
loc.found = False
|
1338
|
-
return loc
|
1170
|
+
loc.found = False # pragma: no cover
|
1171
|
+
return loc # pragma: no cover
|
1339
1172
|
|
1340
1173
|
if (peak := loc.peak_select.split("_")[1]) == "offpeak":
|
1341
1174
|
peak = "off_peak"
|
@@ -1362,7 +1195,7 @@ class SmileHelper:
|
|
1362
1195
|
peak_list: list[str] = ["nl_peak", "nl_offpeak"]
|
1363
1196
|
t_string = "tariff"
|
1364
1197
|
|
1365
|
-
search = self.
|
1198
|
+
search = self._domain_objects
|
1366
1199
|
loc.logs = search.find(f'./location[@id="{loc_id}"]/logs')
|
1367
1200
|
for loc.measurement, loc.attrs in P1_MEASUREMENTS.items():
|
1368
1201
|
for loc.log_type in log_list:
|
@@ -1384,92 +1217,16 @@ class SmileHelper:
|
|
1384
1217
|
self._count += len(direct_data["sensors"])
|
1385
1218
|
return direct_data
|
1386
1219
|
|
1387
|
-
def _power_data_from_modules(self) -> DeviceData:
|
1388
|
-
"""Helper-function for smile.py: _get_device_data().
|
1389
|
-
|
1390
|
-
Collect the power-data from MODULES (P1 legacy only).
|
1391
|
-
"""
|
1392
|
-
direct_data: DeviceData = {"sensors": {}}
|
1393
|
-
loc = Munch()
|
1394
|
-
mod_list: list[str] = ["interval_meter", "cumulative_meter", "point_meter"]
|
1395
|
-
peak_list: list[str] = ["nl_peak", "nl_offpeak"]
|
1396
|
-
t_string = "tariff_indicator"
|
1397
|
-
|
1398
|
-
search = self._modules
|
1399
|
-
mod_logs = search.findall("./module/services")
|
1400
|
-
for loc.measurement, loc.attrs in P1_LEGACY_MEASUREMENTS.items():
|
1401
|
-
loc.meas_list = loc.measurement.split("_")
|
1402
|
-
for loc.logs in mod_logs:
|
1403
|
-
for loc.log_type in mod_list:
|
1404
|
-
for loc.peak_select in peak_list:
|
1405
|
-
loc.locator = (
|
1406
|
-
f"./{loc.meas_list[0]}_{loc.log_type}/measurement"
|
1407
|
-
f'[@directionality="{loc.meas_list[1]}"][@{t_string}="{loc.peak_select}"]'
|
1408
|
-
)
|
1409
|
-
loc = self._power_data_peak_value(loc)
|
1410
|
-
if not loc.found:
|
1411
|
-
continue
|
1412
|
-
|
1413
|
-
direct_data = self.power_data_energy_diff(
|
1414
|
-
loc.measurement, loc.net_string, loc.f_val, direct_data
|
1415
|
-
)
|
1416
|
-
key = cast(SensorType, loc.key_string)
|
1417
|
-
direct_data["sensors"][key] = loc.f_val
|
1418
|
-
|
1419
|
-
self._count += len(direct_data["sensors"])
|
1420
|
-
return direct_data
|
1421
|
-
|
1422
1220
|
def _preset(self, loc_id: str) -> str | None:
|
1423
1221
|
"""Helper-function for smile.py: device_data_climate().
|
1424
1222
|
|
1425
1223
|
Collect the active preset based on Location ID.
|
1426
1224
|
"""
|
1427
|
-
|
1428
|
-
|
1429
|
-
|
1430
|
-
return str(preset.text)
|
1431
|
-
return None
|
1225
|
+
locator = f'./location[@id="{loc_id}"]/preset'
|
1226
|
+
if (preset := self._domain_objects.find(locator)) is not None:
|
1227
|
+
return str(preset.text)
|
1432
1228
|
|
1433
|
-
|
1434
|
-
if (
|
1435
|
-
not (active_rule := etree_to_dict(self._domain_objects.find(locator)))
|
1436
|
-
or "icon" not in active_rule
|
1437
|
-
):
|
1438
|
-
return None
|
1439
|
-
|
1440
|
-
return active_rule["icon"]
|
1441
|
-
|
1442
|
-
def _schedules_legacy(
|
1443
|
-
self,
|
1444
|
-
avail: list[str],
|
1445
|
-
location: str,
|
1446
|
-
sel: str,
|
1447
|
-
) -> tuple[list[str], str]:
|
1448
|
-
"""Helper-function for _schedules().
|
1449
|
-
|
1450
|
-
Collect available schedules/schedules for the legacy thermostat.
|
1451
|
-
"""
|
1452
|
-
name: str | None = None
|
1453
|
-
|
1454
|
-
search = self._domain_objects
|
1455
|
-
for schedule in search.findall("./rule"):
|
1456
|
-
if rule_name := schedule.find("name").text:
|
1457
|
-
if "preset" not in rule_name:
|
1458
|
-
name = rule_name
|
1459
|
-
|
1460
|
-
log_type = "schedule_state"
|
1461
|
-
locator = f"./appliance[type='thermostat']/logs/point_log[type='{log_type}']/period/measurement"
|
1462
|
-
active = False
|
1463
|
-
if (result := search.find(locator)) is not None:
|
1464
|
-
active = result.text == "on"
|
1465
|
-
|
1466
|
-
if name is not None:
|
1467
|
-
avail = [name]
|
1468
|
-
if active:
|
1469
|
-
sel = name
|
1470
|
-
|
1471
|
-
self._last_active[location] = "".join(map(str, avail))
|
1472
|
-
return avail, sel
|
1229
|
+
return None # pragma: no cover
|
1473
1230
|
|
1474
1231
|
def _schedules(self, location: str) -> tuple[list[str], str]:
|
1475
1232
|
"""Helper-function for smile.py: _device_data_climate().
|
@@ -1478,13 +1235,8 @@ class SmileHelper:
|
|
1478
1235
|
NEW: when a location_id is present then the schedule is active. Valid for both Adam and non-legacy Anna.
|
1479
1236
|
"""
|
1480
1237
|
available: list[str] = [NONE]
|
1481
|
-
rule_ids: dict[str, str] = {}
|
1238
|
+
rule_ids: dict[str, dict[str, str]] = {}
|
1482
1239
|
selected = NONE
|
1483
|
-
|
1484
|
-
# Legacy Anna schedule, only one schedule allowed
|
1485
|
-
if self._smile_legacy:
|
1486
|
-
return self._schedules_legacy(available, location, selected)
|
1487
|
-
|
1488
1240
|
# Adam schedules, one schedule can be linked to various locations
|
1489
1241
|
# self._last_active contains the locations and the active schedule name per location, or None
|
1490
1242
|
if location not in self._last_active:
|
@@ -1495,15 +1247,16 @@ class SmileHelper:
|
|
1495
1247
|
return available, selected
|
1496
1248
|
|
1497
1249
|
schedules: list[str] = []
|
1498
|
-
for rule_id,
|
1499
|
-
|
1250
|
+
for rule_id, data in rule_ids.items():
|
1251
|
+
active = data["active"] == "true"
|
1252
|
+
name = data["name"]
|
1500
1253
|
locator = f'./rule[@id="{rule_id}"]/directives'
|
1501
1254
|
# Show an empty schedule as no schedule found
|
1502
1255
|
if self._domain_objects.find(locator) is None:
|
1503
1256
|
continue # pragma: no cover
|
1504
1257
|
|
1505
1258
|
available.append(name)
|
1506
|
-
if location ==
|
1259
|
+
if location == data["location"] and active:
|
1507
1260
|
selected = name
|
1508
1261
|
self._last_active[location] = selected
|
1509
1262
|
schedules.append(name)
|
@@ -1542,7 +1295,6 @@ class SmileHelper:
|
|
1542
1295
|
locator = f'./location[@id="{obj_id}"]/logs/point_log[type="{measurement}"]/period/measurement'
|
1543
1296
|
if (found := search.find(locator)) is not None:
|
1544
1297
|
val = format_measure(found.text, NONE)
|
1545
|
-
return val
|
1546
1298
|
|
1547
1299
|
return val
|
1548
1300
|
|
@@ -1553,9 +1305,6 @@ class SmileHelper:
|
|
1553
1305
|
"""
|
1554
1306
|
actuator = "actuator_functionalities"
|
1555
1307
|
func_type = "relay_functionality"
|
1556
|
-
if self._stretch_v2:
|
1557
|
-
actuator = "actuators"
|
1558
|
-
func_type = "relay"
|
1559
1308
|
if xml.find("type").text not in SPECIAL_PLUG_TYPES:
|
1560
1309
|
locator = f"./{actuator}/{func_type}/lock"
|
1561
1310
|
if (found := xml.find(locator)) is not None:
|
@@ -1574,8 +1323,19 @@ class SmileHelper:
|
|
1574
1323
|
if (state := xml.find(locator)) is not None:
|
1575
1324
|
data["switches"][name] = state.text == "on"
|
1576
1325
|
self._count += 1
|
1577
|
-
|
1578
|
-
|
1579
|
-
|
1580
|
-
|
1581
|
-
|
1326
|
+
|
1327
|
+
def _get_plugwise_notifications(self) -> None:
|
1328
|
+
"""Collect the Plugwise notifications."""
|
1329
|
+
self._notifications = {}
|
1330
|
+
for notification in self._domain_objects.findall("./notification"):
|
1331
|
+
try:
|
1332
|
+
msg_id = notification.attrib["id"]
|
1333
|
+
msg_type = notification.find("type").text
|
1334
|
+
msg = notification.find("message").text
|
1335
|
+
self._notifications.update({msg_id: {msg_type: msg}})
|
1336
|
+
LOGGER.debug("Plugwise notifications: %s", self._notifications)
|
1337
|
+
except AttributeError: # pragma: no cover
|
1338
|
+
LOGGER.debug(
|
1339
|
+
"Plugwise notification present but unable to process, manually investigate: %s",
|
1340
|
+
f"{self._endpoint}{DOMAIN_OBJECTS}",
|
1341
|
+
)
|