plugwise 0.37.1a2__py3-none-any.whl → 0.37.3__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/common.py +194 -16
- plugwise/constants.py +47 -44
- plugwise/data.py +57 -73
- plugwise/helper.py +430 -614
- plugwise/legacy/data.py +28 -44
- plugwise/legacy/helper.py +174 -357
- plugwise/legacy/smile.py +45 -41
- plugwise/smile.py +130 -130
- plugwise/util.py +62 -0
- {plugwise-0.37.1a2.dist-info → plugwise-0.37.3.dist-info}/METADATA +1 -5
- plugwise-0.37.3.dist-info/RECORD +17 -0
- {plugwise-0.37.1a2.dist-info → plugwise-0.37.3.dist-info}/WHEEL +1 -1
- plugwise-0.37.1a2.dist-info/RECORD +0 -17
- {plugwise-0.37.1a2.dist-info → plugwise-0.37.3.dist-info}/LICENSE +0 -0
- {plugwise-0.37.1a2.dist-info → plugwise-0.37.3.dist-info}/top_level.txt +0 -0
plugwise/legacy/helper.py
CHANGED
@@ -4,14 +4,12 @@ Plugwise Smile protocol helpers.
|
|
4
4
|
"""
|
5
5
|
from __future__ import annotations
|
6
6
|
|
7
|
-
import datetime as dt
|
8
7
|
from typing import cast
|
9
8
|
|
10
9
|
from plugwise.common import SmileCommon
|
11
10
|
from plugwise.constants import (
|
12
11
|
ACTIVE_ACTUATORS,
|
13
12
|
ACTUATOR_CLASSES,
|
14
|
-
ANNA,
|
15
13
|
APPLIANCES,
|
16
14
|
ATTR_NAME,
|
17
15
|
ATTR_UNIT_OF_MEASUREMENT,
|
@@ -24,12 +22,9 @@ from plugwise.constants import (
|
|
24
22
|
HEATER_CENTRAL_MEASUREMENTS,
|
25
23
|
LIMITS,
|
26
24
|
NONE,
|
27
|
-
OBSOLETE_MEASUREMENTS,
|
28
25
|
P1_LEGACY_MEASUREMENTS,
|
29
26
|
SENSORS,
|
30
|
-
SPECIAL_PLUG_TYPES,
|
31
27
|
SPECIALS,
|
32
|
-
SWITCH_GROUP_TYPES,
|
33
28
|
SWITCHES,
|
34
29
|
TEMP_CELSIUS,
|
35
30
|
THERMOSTAT_CLASSES,
|
@@ -46,7 +41,7 @@ from plugwise.constants import (
|
|
46
41
|
SwitchType,
|
47
42
|
ThermoLoc,
|
48
43
|
)
|
49
|
-
from plugwise.util import format_measure,
|
44
|
+
from plugwise.util import format_measure, skip_obsolete_measurements, version_to_model
|
50
45
|
|
51
46
|
# This way of importing aiohttp is because of patch/mocking in testing (aiohttp timeouts)
|
52
47
|
from defusedxml import ElementTree as etree
|
@@ -97,6 +92,60 @@ class SmileLegacyHelper(SmileCommon):
|
|
97
92
|
self.smile_zigbee_mac_address: str | None
|
98
93
|
SmileCommon.__init__(self)
|
99
94
|
|
95
|
+
def _all_appliances(self) -> None:
|
96
|
+
"""Collect all appliances with relevant info."""
|
97
|
+
self._count = 0
|
98
|
+
self._all_locations()
|
99
|
+
|
100
|
+
self._create_legacy_gateway()
|
101
|
+
# For legacy P1 collect the connected SmartMeter info
|
102
|
+
if self.smile_type == "power":
|
103
|
+
appl = Munch()
|
104
|
+
self._p1_smartmeter_info_finder(appl)
|
105
|
+
# Legacy P1 has no more devices
|
106
|
+
return
|
107
|
+
|
108
|
+
for appliance in self._appliances.findall("./appliance"):
|
109
|
+
appl = Munch()
|
110
|
+
appl.pwclass = appliance.find("type").text
|
111
|
+
# Skip thermostats that have this key, should be an orphaned device (Core #81712)
|
112
|
+
if (
|
113
|
+
appl.pwclass == "thermostat"
|
114
|
+
and appliance.find("actuator_functionalities/") is None
|
115
|
+
):
|
116
|
+
continue # pragma: no cover
|
117
|
+
|
118
|
+
appl.location = self._home_location
|
119
|
+
appl.dev_id = appliance.attrib["id"]
|
120
|
+
appl.name = appliance.find("name").text
|
121
|
+
appl.model = appl.pwclass.replace("_", " ").title()
|
122
|
+
appl.firmware = None
|
123
|
+
appl.hardware = None
|
124
|
+
appl.mac = None
|
125
|
+
appl.zigbee_mac = None
|
126
|
+
appl.vendor_name = None
|
127
|
+
|
128
|
+
# Determine class for this appliance
|
129
|
+
# Skip on heater_central when no active device present or on orphaned stretch devices
|
130
|
+
if not (appl := self._appliance_info_finder(appliance, appl)):
|
131
|
+
continue
|
132
|
+
|
133
|
+
# Skip orphaned heater_central (Core Issue #104433)
|
134
|
+
if appl.pwclass == "heater_central" and appl.dev_id != self._heater_id:
|
135
|
+
continue # pragma: no cover
|
136
|
+
|
137
|
+
self._create_gw_devices(appl)
|
138
|
+
|
139
|
+
# Place the gateway and optional heater_central devices as 1st and 2nd
|
140
|
+
for dev_class in ("heater_central", "gateway"):
|
141
|
+
for dev_id, device in dict(self.gw_devices).items():
|
142
|
+
if device["dev_class"] == dev_class:
|
143
|
+
tmp_device = device
|
144
|
+
self.gw_devices.pop(dev_id)
|
145
|
+
cleared_dict = self.gw_devices
|
146
|
+
add_to_front = {dev_id: tmp_device}
|
147
|
+
self.gw_devices = {**add_to_front, **cleared_dict}
|
148
|
+
|
100
149
|
def _all_locations(self) -> None:
|
101
150
|
"""Collect all locations."""
|
102
151
|
loc = Munch()
|
@@ -127,6 +176,44 @@ class SmileLegacyHelper(SmileCommon):
|
|
127
176
|
|
128
177
|
self.loc_data[loc.loc_id] = {"name": loc.name}
|
129
178
|
|
179
|
+
def _create_legacy_gateway(self) -> None:
|
180
|
+
"""Create the (missing) gateway devices for legacy Anna, P1 and Stretch.
|
181
|
+
|
182
|
+
Use the home_location or FAKE_APPL as device id.
|
183
|
+
"""
|
184
|
+
self.gateway_id = self._home_location
|
185
|
+
if self.smile_type == "power":
|
186
|
+
self.gateway_id = FAKE_APPL
|
187
|
+
|
188
|
+
self.gw_devices[self.gateway_id] = {"dev_class": "gateway"}
|
189
|
+
self._count += 1
|
190
|
+
for key, value in {
|
191
|
+
"firmware": self.smile_fw_version,
|
192
|
+
"location": self._home_location,
|
193
|
+
"mac_address": self.smile_mac_address,
|
194
|
+
"model": self.smile_model,
|
195
|
+
"name": self.smile_name,
|
196
|
+
"zigbee_mac_address": self.smile_zigbee_mac_address,
|
197
|
+
"vendor": "Plugwise",
|
198
|
+
}.items():
|
199
|
+
if value is not None:
|
200
|
+
gw_key = cast(ApplianceType, key)
|
201
|
+
self.gw_devices[self.gateway_id][gw_key] = value
|
202
|
+
self._count += 1
|
203
|
+
|
204
|
+
def _appliance_info_finder(self, appliance: etree, appl: Munch) -> Munch:
|
205
|
+
"""Collect device info (Smile/Stretch, Thermostats, OpenTherm/On-Off): firmware, model and vendor name."""
|
206
|
+
match appl.pwclass:
|
207
|
+
# Collect thermostat device info
|
208
|
+
case _ as dev_class if dev_class in THERMOSTAT_CLASSES:
|
209
|
+
return self._appl_thermostat_info(appl, appliance, self._modules)
|
210
|
+
# Collect heater_central device info
|
211
|
+
case "heater_central":
|
212
|
+
return self._appl_heater_central_info(appl, appliance, self._appliances, self._modules)
|
213
|
+
# Collect info from Stretches
|
214
|
+
case _:
|
215
|
+
return self._energy_device_info_finder(appliance, appl)
|
216
|
+
|
130
217
|
def _energy_device_info_finder(self, appliance: etree, appl: Munch) -> Munch:
|
131
218
|
"""Helper-function for _appliance_info_finder().
|
132
219
|
|
@@ -154,21 +241,6 @@ class SmileLegacyHelper(SmileCommon):
|
|
154
241
|
|
155
242
|
return appl # pragma: no cover
|
156
243
|
|
157
|
-
def _appliance_info_finder(self, appliance: etree, appl: Munch) -> Munch:
|
158
|
-
"""Collect device info (Smile/Stretch, Thermostats, OpenTherm/On-Off): firmware, model and vendor name."""
|
159
|
-
# Collect thermostat device info
|
160
|
-
if appl.pwclass in THERMOSTAT_CLASSES:
|
161
|
-
return self._appl_thermostat_info(appl, appliance, self._modules)
|
162
|
-
|
163
|
-
# Collect heater_central device info
|
164
|
-
if appl.pwclass == "heater_central":
|
165
|
-
return self._appl_heater_central_info(appl, appliance, self._appliances, self._modules)
|
166
|
-
|
167
|
-
# Collect info from Stretches
|
168
|
-
appl = self._energy_device_info_finder(appliance, appl)
|
169
|
-
|
170
|
-
return appl
|
171
|
-
|
172
244
|
def _p1_smartmeter_info_finder(self, appl: Munch) -> None:
|
173
245
|
"""Collect P1 DSMR Smartmeter info."""
|
174
246
|
loc_id = next(iter(self.loc_data.keys()))
|
@@ -182,130 +254,72 @@ class SmileLegacyHelper(SmileCommon):
|
|
182
254
|
location = self._locations.find(f'./location[@id="{loc_id}"]')
|
183
255
|
appl = self._energy_device_info_finder(location, appl)
|
184
256
|
|
185
|
-
self.
|
186
|
-
self._count += 1
|
257
|
+
self._create_gw_devices(appl)
|
187
258
|
|
188
|
-
|
189
|
-
|
190
|
-
"hardware": appl.hardware,
|
191
|
-
"location": appl.location,
|
192
|
-
"mac_address": appl.mac,
|
193
|
-
"model": appl.model,
|
194
|
-
"name": appl.name,
|
195
|
-
"zigbee_mac_address": appl.zigbee_mac,
|
196
|
-
"vendor": appl.vendor_name,
|
197
|
-
}.items():
|
198
|
-
if value is not None or key == "location":
|
199
|
-
p1_key = cast(ApplianceType, key)
|
200
|
-
self.gw_devices[appl.dev_id][p1_key] = value
|
201
|
-
self._count += 1
|
202
|
-
|
203
|
-
def _create_legacy_gateway(self) -> None:
|
204
|
-
"""Create the (missing) gateway devices for legacy Anna, P1 and Stretch.
|
259
|
+
def _get_measurement_data(self, dev_id: str) -> DeviceData:
|
260
|
+
"""Helper-function for smile.py: _get_device_data().
|
205
261
|
|
206
|
-
|
262
|
+
Collect the appliance-data based on device id.
|
207
263
|
"""
|
208
|
-
|
264
|
+
data: DeviceData = {"binary_sensors": {}, "sensors": {}, "switches": {}}
|
265
|
+
# Get P1 smartmeter data from LOCATIONS or MODULES
|
266
|
+
device = self.gw_devices[dev_id]
|
267
|
+
# !! DON'T CHANGE below two if-lines, will break stuff !!
|
209
268
|
if self.smile_type == "power":
|
210
|
-
|
269
|
+
if device["dev_class"] == "smartmeter":
|
270
|
+
data.update(self._power_data_from_modules())
|
211
271
|
|
212
|
-
|
213
|
-
self._count += 1
|
214
|
-
for key, value in {
|
215
|
-
"firmware": self.smile_fw_version,
|
216
|
-
"location": self._home_location,
|
217
|
-
"mac_address": self.smile_mac_address,
|
218
|
-
"model": self.smile_model,
|
219
|
-
"name": self.smile_name,
|
220
|
-
"zigbee_mac_address": self.smile_zigbee_mac_address,
|
221
|
-
"vendor": "Plugwise",
|
222
|
-
}.items():
|
223
|
-
if value is not None:
|
224
|
-
gw_key = cast(ApplianceType, key)
|
225
|
-
self.gw_devices[self.gateway_id][gw_key] = value
|
226
|
-
self._count += 1
|
272
|
+
return data
|
227
273
|
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
self._all_locations()
|
274
|
+
measurements = DEVICE_MEASUREMENTS
|
275
|
+
if self._is_thermostat and dev_id == self._heater_id:
|
276
|
+
measurements = HEATER_CENTRAL_MEASUREMENTS
|
232
277
|
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
self.
|
238
|
-
# Legacy P1 has no more devices
|
239
|
-
return
|
278
|
+
if (
|
279
|
+
appliance := self._appliances.find(f'./appliance[@id="{dev_id}"]')
|
280
|
+
) is not None:
|
281
|
+
self._appliance_measurements(appliance, data, measurements)
|
282
|
+
self._get_lock_state(appliance, data, self._stretch_v2)
|
240
283
|
|
241
|
-
|
242
|
-
|
243
|
-
appl.pwclass = appliance.find("type").text
|
244
|
-
# Skip thermostats that have this key, should be an orphaned device (Core #81712)
|
245
|
-
if (
|
246
|
-
appl.pwclass == "thermostat"
|
247
|
-
and appliance.find("actuator_functionalities/") is None
|
248
|
-
):
|
249
|
-
continue # pragma: no cover
|
284
|
+
if appliance.find("type").text in ACTUATOR_CLASSES:
|
285
|
+
self._get_actuator_functionalities(appliance, device, data)
|
250
286
|
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
287
|
+
# Adam & Anna: the Smile outdoor_temperature is present in DOMAIN_OBJECTS and LOCATIONS - under Home
|
288
|
+
# The outdoor_temperature present in APPLIANCES is a local sensor connected to the active device
|
289
|
+
if self._is_thermostat and dev_id == self.gateway_id:
|
290
|
+
outdoor_temperature = self._object_value(
|
291
|
+
self._home_location, "outdoor_temperature"
|
292
|
+
)
|
293
|
+
if outdoor_temperature is not None:
|
294
|
+
data.update({"sensors": {"outdoor_temperature": outdoor_temperature}})
|
295
|
+
self._count += 1
|
260
296
|
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
continue
|
297
|
+
if "c_heating_state" in data:
|
298
|
+
data.pop("c_heating_state")
|
299
|
+
self._count -= 1
|
265
300
|
|
266
|
-
|
267
|
-
if appl.pwclass == "heater_central" and appl.dev_id != self._heater_id:
|
268
|
-
continue # pragma: no cover
|
301
|
+
return data
|
269
302
|
|
270
|
-
|
271
|
-
|
272
|
-
for key, value in {
|
273
|
-
"firmware": appl.firmware,
|
274
|
-
"hardware": appl.hardware,
|
275
|
-
"location": appl.location,
|
276
|
-
"mac_address": appl.mac,
|
277
|
-
"model": appl.model,
|
278
|
-
"name": appl.name,
|
279
|
-
"zigbee_mac_address": appl.zigbee_mac,
|
280
|
-
"vendor": appl.vendor_name,
|
281
|
-
}.items():
|
282
|
-
if value is not None or key == "location":
|
283
|
-
appl_key = cast(ApplianceType, key)
|
284
|
-
self.gw_devices[appl.dev_id][appl_key] = value
|
285
|
-
self._count += 1
|
303
|
+
def _power_data_from_modules(self) -> DeviceData:
|
304
|
+
"""Helper-function for smile.py: _get_device_data().
|
286
305
|
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
cleared_dict = self.gw_devices
|
294
|
-
add_to_front = {dev_id: tmp_device}
|
295
|
-
self.gw_devices = {**add_to_front, **cleared_dict}
|
306
|
+
Collect the power-data from MODULES (P1 legacy only).
|
307
|
+
"""
|
308
|
+
direct_data: DeviceData = {"sensors": {}}
|
309
|
+
loc = Munch()
|
310
|
+
mod_list: list[str] = ["interval_meter", "cumulative_meter", "point_meter"]
|
311
|
+
t_string = "tariff_indicator"
|
296
312
|
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
float(directive.attrib["temperature"]),
|
305
|
-
0,
|
306
|
-
]
|
313
|
+
search = self._modules
|
314
|
+
mod_logs = search.findall("./module/services")
|
315
|
+
for loc.measurement, loc.attrs in P1_LEGACY_MEASUREMENTS.items():
|
316
|
+
loc.meas_list = loc.measurement.split("_")
|
317
|
+
for loc.logs in mod_logs:
|
318
|
+
for loc.log_type in mod_list:
|
319
|
+
self._collect_power_values(direct_data, loc, t_string, legacy=True)
|
307
320
|
|
308
|
-
|
321
|
+
self._count += len(direct_data["sensors"])
|
322
|
+
return direct_data
|
309
323
|
|
310
324
|
def _appliance_measurements(
|
311
325
|
self,
|
@@ -320,20 +334,8 @@ class SmileLegacyHelper(SmileCommon):
|
|
320
334
|
if measurement == "domestic_hot_water_state":
|
321
335
|
continue
|
322
336
|
|
323
|
-
|
324
|
-
|
325
|
-
f'.//logs/point_log[type="{measurement}"]/updated_date'
|
326
|
-
)
|
327
|
-
if (
|
328
|
-
measurement in OBSOLETE_MEASUREMENTS
|
329
|
-
and (updated_date_key := appliance.find(updated_date_locator))
|
330
|
-
is not None
|
331
|
-
):
|
332
|
-
updated_date = updated_date_key.text.split("T")[0]
|
333
|
-
date_1 = dt.datetime.strptime(updated_date, "%Y-%m-%d")
|
334
|
-
date_2 = dt.datetime.now()
|
335
|
-
if int((date_2 - date_1).days) > 7:
|
336
|
-
continue # pragma: no cover
|
337
|
+
if skip_obsolete_measurements(appliance, measurement):
|
338
|
+
continue # pragma: no cover
|
337
339
|
|
338
340
|
if new_name := getattr(attrs, ATTR_NAME, None):
|
339
341
|
measurement = new_name
|
@@ -406,194 +408,19 @@ class SmileLegacyHelper(SmileCommon):
|
|
406
408
|
act_item = cast(ActuatorType, item)
|
407
409
|
data[act_item] = temp_dict
|
408
410
|
|
409
|
-
def
|
410
|
-
"""Helper-function for smile.py: _get_device_data().
|
411
|
-
|
412
|
-
Collect the appliance-data based on device id.
|
413
|
-
"""
|
414
|
-
data: DeviceData = {"binary_sensors": {}, "sensors": {}, "switches": {}}
|
415
|
-
# Get P1 smartmeter data from LOCATIONS or MODULES
|
416
|
-
device = self.gw_devices[dev_id]
|
417
|
-
# !! DON'T CHANGE below two if-lines, will break stuff !!
|
418
|
-
if self.smile_type == "power":
|
419
|
-
if device["dev_class"] == "smartmeter":
|
420
|
-
data.update(self._power_data_from_modules())
|
421
|
-
|
422
|
-
return data
|
423
|
-
|
424
|
-
measurements = DEVICE_MEASUREMENTS
|
425
|
-
if self._is_thermostat and dev_id == self._heater_id:
|
426
|
-
measurements = HEATER_CENTRAL_MEASUREMENTS
|
427
|
-
|
428
|
-
if (
|
429
|
-
appliance := self._appliances.find(f'./appliance[@id="{dev_id}"]')
|
430
|
-
) is not None:
|
431
|
-
self._appliance_measurements(appliance, data, measurements)
|
432
|
-
self._get_lock_state(appliance, data)
|
433
|
-
|
434
|
-
if appliance.find("type").text in ACTUATOR_CLASSES:
|
435
|
-
self._get_actuator_functionalities(appliance, device, data)
|
436
|
-
|
437
|
-
# Adam & Anna: the Smile outdoor_temperature is present in DOMAIN_OBJECTS and LOCATIONS - under Home
|
438
|
-
# The outdoor_temperature present in APPLIANCES is a local sensor connected to the active device
|
439
|
-
if self._is_thermostat and dev_id == self.gateway_id:
|
440
|
-
outdoor_temperature = self._object_value(
|
441
|
-
self._home_location, "outdoor_temperature"
|
442
|
-
)
|
443
|
-
if outdoor_temperature is not None:
|
444
|
-
data.update({"sensors": {"outdoor_temperature": outdoor_temperature}})
|
445
|
-
self._count += 1
|
446
|
-
|
447
|
-
if "c_heating_state" in data:
|
448
|
-
data.pop("c_heating_state")
|
449
|
-
self._count -= 1
|
450
|
-
|
451
|
-
return data
|
452
|
-
|
453
|
-
def _thermostat_uri(self) -> str:
|
454
|
-
"""Determine the location-set_temperature uri - from APPLIANCES."""
|
455
|
-
locator = "./appliance[type='thermostat']"
|
456
|
-
appliance_id = self._appliances.find(locator).attrib["id"]
|
457
|
-
|
458
|
-
return f"{APPLIANCES};id={appliance_id}/thermostat"
|
459
|
-
|
460
|
-
def _get_group_switches(self) -> dict[str, DeviceData]:
|
461
|
-
"""Helper-function for smile.py: get_all_devices().
|
462
|
-
|
463
|
-
Collect switching- or pump-group info.
|
464
|
-
"""
|
465
|
-
switch_groups: dict[str, DeviceData] = {}
|
466
|
-
# P1 and Anna don't have switchgroups
|
467
|
-
if self.smile_type == "power" or self.smile(ANNA):
|
468
|
-
return switch_groups
|
469
|
-
|
470
|
-
for group in self._domain_objects.findall("./group"):
|
471
|
-
members: list[str] = []
|
472
|
-
group_id = group.attrib["id"]
|
473
|
-
group_name = group.find("name").text
|
474
|
-
group_type = group.find("type").text
|
475
|
-
group_appliances = group.findall("appliances/appliance")
|
476
|
-
for item in group_appliances:
|
477
|
-
# Check if members are not orphaned - stretch
|
478
|
-
if item.attrib["id"] in self.gw_devices:
|
479
|
-
members.append(item.attrib["id"])
|
480
|
-
|
481
|
-
if group_type in SWITCH_GROUP_TYPES and members:
|
482
|
-
switch_groups.update(
|
483
|
-
{
|
484
|
-
group_id: {
|
485
|
-
"dev_class": group_type,
|
486
|
-
"model": "Switchgroup",
|
487
|
-
"name": group_name,
|
488
|
-
"members": members,
|
489
|
-
},
|
490
|
-
},
|
491
|
-
)
|
492
|
-
self._count += 4
|
493
|
-
|
494
|
-
return switch_groups
|
495
|
-
|
496
|
-
def power_data_energy_diff(
|
497
|
-
self,
|
498
|
-
measurement: str,
|
499
|
-
net_string: SensorType,
|
500
|
-
f_val: float | int,
|
501
|
-
direct_data: DeviceData,
|
502
|
-
) -> DeviceData:
|
503
|
-
"""Calculate differential energy."""
|
504
|
-
if (
|
505
|
-
"electricity" in measurement
|
506
|
-
and "phase" not in measurement
|
507
|
-
and "interval" not in net_string
|
508
|
-
):
|
509
|
-
diff = 1
|
510
|
-
if "produced" in measurement:
|
511
|
-
diff = -1
|
512
|
-
if net_string not in direct_data["sensors"]:
|
513
|
-
tmp_val: float | int = 0
|
514
|
-
else:
|
515
|
-
tmp_val = direct_data["sensors"][net_string]
|
516
|
-
|
517
|
-
if isinstance(f_val, int):
|
518
|
-
tmp_val += f_val * diff
|
519
|
-
else:
|
520
|
-
tmp_val += float(f_val * diff)
|
521
|
-
tmp_val = float(f"{round(tmp_val, 3):.3f}")
|
522
|
-
|
523
|
-
direct_data["sensors"][net_string] = tmp_val
|
524
|
-
|
525
|
-
return direct_data
|
526
|
-
|
527
|
-
def _power_data_peak_value(self, loc: Munch) -> Munch:
|
528
|
-
"""Helper-function for _power_data_from_location() and _power_data_from_modules()."""
|
529
|
-
loc.found = True
|
530
|
-
# If locator not found for P1 legacy electricity_point_meter or gas_*_meter data
|
531
|
-
if loc.logs.find(loc.locator) is None:
|
532
|
-
if "meter" in loc.log_type and (
|
533
|
-
"point" in loc.log_type or "gas" in loc.measurement
|
534
|
-
):
|
535
|
-
# Avoid double processing by skipping one peak-list option
|
536
|
-
if loc.peak_select == "nl_offpeak":
|
537
|
-
loc.found = False
|
538
|
-
return loc
|
539
|
-
|
540
|
-
loc.locator = (
|
541
|
-
f"./{loc.meas_list[0]}_{loc.log_type}/"
|
542
|
-
f'measurement[@directionality="{loc.meas_list[1]}"]'
|
543
|
-
)
|
544
|
-
if loc.logs.find(loc.locator) is None:
|
545
|
-
loc.found = False
|
546
|
-
return loc
|
547
|
-
else:
|
548
|
-
loc.found = False
|
549
|
-
return loc
|
550
|
-
|
551
|
-
if (peak := loc.peak_select.split("_")[1]) == "offpeak":
|
552
|
-
peak = "off_peak"
|
553
|
-
log_found = loc.log_type.split("_")[0]
|
554
|
-
loc.key_string = f"{loc.measurement}_{peak}_{log_found}"
|
555
|
-
if "gas" in loc.measurement or loc.log_type == "point_meter":
|
556
|
-
loc.key_string = f"{loc.measurement}_{log_found}"
|
557
|
-
loc.net_string = f"net_electricity_{log_found}"
|
558
|
-
val = loc.logs.find(loc.locator).text
|
559
|
-
loc.f_val = power_data_local_format(loc.attrs, loc.key_string, val)
|
560
|
-
|
561
|
-
return loc
|
562
|
-
|
563
|
-
def _power_data_from_modules(self) -> DeviceData:
|
564
|
-
"""Helper-function for smile.py: _get_device_data().
|
411
|
+
def _object_value(self, obj_id: str, measurement: str) -> float | int | None:
|
412
|
+
"""Helper-function for smile.py: _get_device_data() and _device_data_anna().
|
565
413
|
|
566
|
-
|
414
|
+
Obtain the value/state for the given object from a location in DOMAIN_OBJECTS
|
567
415
|
"""
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
search = self._modules
|
575
|
-
mod_logs = search.findall("./module/services")
|
576
|
-
for loc.measurement, loc.attrs in P1_LEGACY_MEASUREMENTS.items():
|
577
|
-
loc.meas_list = loc.measurement.split("_")
|
578
|
-
for loc.logs in mod_logs:
|
579
|
-
for loc.log_type in mod_list:
|
580
|
-
for loc.peak_select in peak_list:
|
581
|
-
loc.locator = (
|
582
|
-
f"./{loc.meas_list[0]}_{loc.log_type}/measurement"
|
583
|
-
f'[@directionality="{loc.meas_list[1]}"][@{t_string}="{loc.peak_select}"]'
|
584
|
-
)
|
585
|
-
loc = self._power_data_peak_value(loc)
|
586
|
-
if not loc.found:
|
587
|
-
continue
|
588
|
-
|
589
|
-
direct_data = self.power_data_energy_diff(
|
590
|
-
loc.measurement, loc.net_string, loc.f_val, direct_data
|
591
|
-
)
|
592
|
-
key = cast(SensorType, loc.key_string)
|
593
|
-
direct_data["sensors"][key] = loc.f_val
|
416
|
+
val: float | int | None = None
|
417
|
+
search = self._domain_objects
|
418
|
+
locator = f'./location[@id="{obj_id}"]/logs/point_log[type="{measurement}"]/period/measurement'
|
419
|
+
if (found := search.find(locator)) is not None:
|
420
|
+
val = format_measure(found.text, NONE)
|
421
|
+
return val
|
594
422
|
|
595
|
-
|
596
|
-
return direct_data
|
423
|
+
return val
|
597
424
|
|
598
425
|
def _preset(self) -> str | None:
|
599
426
|
"""Helper-function for smile.py: device_data_climate().
|
@@ -609,6 +436,19 @@ class SmileLegacyHelper(SmileCommon):
|
|
609
436
|
|
610
437
|
return active_rule["icon"]
|
611
438
|
|
439
|
+
def _presets(self) -> dict[str, list[float]]:
|
440
|
+
"""Helper-function for presets() - collect Presets for a legacy Anna."""
|
441
|
+
presets: dict[str, list[float]] = {}
|
442
|
+
for directive in self._domain_objects.findall("rule/directives/when/then"):
|
443
|
+
if directive is not None and directive.get("icon") is not None:
|
444
|
+
# Ensure list of heating_setpoint, cooling_setpoint
|
445
|
+
presets[directive.attrib["icon"]] = [
|
446
|
+
float(directive.attrib["temperature"]),
|
447
|
+
0,
|
448
|
+
]
|
449
|
+
|
450
|
+
return presets
|
451
|
+
|
612
452
|
def _schedules(self) -> tuple[list[str], str]:
|
613
453
|
"""Collect available schedules/schedules for the legacy thermostat."""
|
614
454
|
available: list[str] = [NONE]
|
@@ -634,32 +474,9 @@ class SmileLegacyHelper(SmileCommon):
|
|
634
474
|
|
635
475
|
return available, selected
|
636
476
|
|
637
|
-
def
|
638
|
-
"""
|
639
|
-
|
640
|
-
|
641
|
-
"""
|
642
|
-
val: float | int | None = None
|
643
|
-
search = self._domain_objects
|
644
|
-
locator = f'./location[@id="{obj_id}"]/logs/point_log[type="{measurement}"]/period/measurement'
|
645
|
-
if (found := search.find(locator)) is not None:
|
646
|
-
val = format_measure(found.text, NONE)
|
647
|
-
return val
|
648
|
-
|
649
|
-
return val
|
650
|
-
|
651
|
-
def _get_lock_state(self, xml: etree, data: DeviceData) -> None:
|
652
|
-
"""Helper-function for _get_measurement_data().
|
477
|
+
def _thermostat_uri(self) -> str:
|
478
|
+
"""Determine the location-set_temperature uri - from APPLIANCES."""
|
479
|
+
locator = "./appliance[type='thermostat']"
|
480
|
+
appliance_id = self._appliances.find(locator).attrib["id"]
|
653
481
|
|
654
|
-
|
655
|
-
"""
|
656
|
-
actuator = "actuator_functionalities"
|
657
|
-
func_type = "relay_functionality"
|
658
|
-
if self._stretch_v2:
|
659
|
-
actuator = "actuators"
|
660
|
-
func_type = "relay"
|
661
|
-
if xml.find("type").text not in SPECIAL_PLUG_TYPES:
|
662
|
-
locator = f"./{actuator}/{func_type}/lock"
|
663
|
-
if (found := xml.find(locator)) is not None:
|
664
|
-
data["switches"]["lock"] = found.text == "true"
|
665
|
-
self._count += 1
|
482
|
+
return f"{APPLIANCES};id={appliance_id}/thermostat"
|