plugwise 1.7.1__py3-none-any.whl → 1.7.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/__init__.py +14 -21
- plugwise/common.py +19 -9
- plugwise/constants.py +0 -11
- plugwise/data.py +10 -21
- plugwise/helper.py +29 -18
- plugwise/legacy/data.py +2 -12
- plugwise/legacy/helper.py +28 -15
- plugwise/legacy/smile.py +61 -29
- plugwise/smile.py +71 -40
- plugwise/smilecomm.py +4 -2
- plugwise/util.py +26 -35
- {plugwise-1.7.1.dist-info → plugwise-1.7.3.dist-info}/METADATA +4 -3
- plugwise-1.7.3.dist-info/RECORD +18 -0
- {plugwise-1.7.1.dist-info → plugwise-1.7.3.dist-info}/WHEEL +1 -1
- plugwise-1.7.1.dist-info/RECORD +0 -18
- {plugwise-1.7.1.dist-info → plugwise-1.7.3.dist-info}/LICENSE +0 -0
- {plugwise-1.7.1.dist-info → plugwise-1.7.3.dist-info}/top_level.txt +0 -0
plugwise/__init__.py
CHANGED
@@ -18,7 +18,6 @@ from plugwise.constants import (
|
|
18
18
|
STATUS,
|
19
19
|
SYSTEM,
|
20
20
|
GwEntityData,
|
21
|
-
SmileProps,
|
22
21
|
ThermoLoc,
|
23
22
|
)
|
24
23
|
from plugwise.exceptions import (
|
@@ -69,7 +68,6 @@ class Smile(SmileComm):
|
|
69
68
|
self._opentherm_device = False
|
70
69
|
self._schedule_old_states: dict[str, dict[str, str]] = {}
|
71
70
|
self._smile_api: SmileAPI | SmileLegacyAPI
|
72
|
-
self._smile_props: SmileProps = {}
|
73
71
|
self._stretch_v2 = False
|
74
72
|
self._target_smile: str = NONE
|
75
73
|
self.smile_hostname: str = NONE
|
@@ -86,26 +84,22 @@ class Smile(SmileComm):
|
|
86
84
|
@property
|
87
85
|
def cooling_present(self) -> bool:
|
88
86
|
"""Return the cooling capability."""
|
89
|
-
|
90
|
-
return self._smile_props["cooling_present"]
|
91
|
-
return False
|
87
|
+
return self._smile_api.cooling_present
|
92
88
|
|
93
89
|
@property
|
94
90
|
def gateway_id(self) -> str:
|
95
91
|
"""Return the gateway-id."""
|
96
|
-
return self.
|
92
|
+
return self._smile_api.gateway_id
|
97
93
|
|
98
94
|
@property
|
99
95
|
def heater_id(self) -> str:
|
100
96
|
"""Return the heater-id."""
|
101
|
-
|
102
|
-
return self._smile_props["heater_id"]
|
103
|
-
return NONE
|
97
|
+
return self._smile_api.heater_id
|
104
98
|
|
105
99
|
@property
|
106
100
|
def item_count(self) -> int:
|
107
101
|
"""Return the item-count."""
|
108
|
-
return self.
|
102
|
+
return self._smile_api.item_count
|
109
103
|
|
110
104
|
@property
|
111
105
|
def reboot(self) -> bool:
|
@@ -162,7 +156,6 @@ class Smile(SmileComm):
|
|
162
156
|
self._opentherm_device,
|
163
157
|
self._request,
|
164
158
|
self._schedule_old_states,
|
165
|
-
self._smile_props,
|
166
159
|
self.smile_hostname,
|
167
160
|
self.smile_hw_version,
|
168
161
|
self.smile_mac_address,
|
@@ -179,7 +172,6 @@ class Smile(SmileComm):
|
|
179
172
|
self._on_off_device,
|
180
173
|
self._opentherm_device,
|
181
174
|
self._request,
|
182
|
-
self._smile_props,
|
183
175
|
self._stretch_v2,
|
184
176
|
self._target_smile,
|
185
177
|
self.smile_hostname,
|
@@ -198,7 +190,9 @@ class Smile(SmileComm):
|
|
198
190
|
|
199
191
|
return self.smile_version
|
200
192
|
|
201
|
-
async def _smile_detect(
|
193
|
+
async def _smile_detect(
|
194
|
+
self, result: etree.Element, dsmrmain: etree.Element
|
195
|
+
) -> None:
|
202
196
|
"""Helper-function for connect().
|
203
197
|
|
204
198
|
Detect which type of Plugwise Gateway is being connected.
|
@@ -256,10 +250,8 @@ class Smile(SmileComm):
|
|
256
250
|
# For Adam, Anna, determine the system capabilities:
|
257
251
|
# Find the connected heating/cooling device (heater_central),
|
258
252
|
# e.g. heat-pump or gas-fired heater
|
259
|
-
onoff_boiler
|
260
|
-
open_therm_boiler
|
261
|
-
"./module/protocols/open_therm_boiler"
|
262
|
-
)
|
253
|
+
onoff_boiler = result.find("./module/protocols/onoff_boiler")
|
254
|
+
open_therm_boiler = result.find("./module/protocols/open_therm_boiler")
|
263
255
|
self._on_off_device = onoff_boiler is not None
|
264
256
|
self._opentherm_device = open_therm_boiler is not None
|
265
257
|
|
@@ -272,7 +264,7 @@ class Smile(SmileComm):
|
|
272
264
|
self._elga = True
|
273
265
|
|
274
266
|
async def _smile_detect_legacy(
|
275
|
-
self, result: etree, dsmrmain: etree, model: str
|
267
|
+
self, result: etree.Element, dsmrmain: etree.Element, model: str
|
276
268
|
) -> str:
|
277
269
|
"""Helper-function for _smile_detect().
|
278
270
|
|
@@ -296,18 +288,19 @@ class Smile(SmileComm):
|
|
296
288
|
):
|
297
289
|
system = await self._request(SYSTEM)
|
298
290
|
self.smile_version = parse(system.find("./gateway/firmware").text)
|
299
|
-
return_model = system.find("./gateway/product").text
|
291
|
+
return_model = str(system.find("./gateway/product").text)
|
300
292
|
self.smile_hostname = system.find("./gateway/hostname").text
|
301
|
-
# If wlan0 contains data it's active,
|
293
|
+
# If wlan0 contains data it's active, eth0 should be checked last as is preferred
|
302
294
|
for network in ("wlan0", "eth0"):
|
303
295
|
locator = f"./{network}/mac"
|
304
296
|
if (net_locator := system.find(locator)) is not None:
|
305
297
|
self.smile_mac_address = net_locator.text
|
298
|
+
|
306
299
|
# P1 legacy:
|
307
300
|
elif dsmrmain is not None:
|
308
301
|
status = await self._request(STATUS)
|
309
302
|
self.smile_version = parse(status.find("./system/version").text)
|
310
|
-
return_model = status.find("./system/product").text
|
303
|
+
return_model = str(status.find("./system/product").text)
|
311
304
|
self.smile_hostname = status.find("./network/hostname").text
|
312
305
|
self.smile_mac_address = status.find("./network/mac_address").text
|
313
306
|
else: # pragma: no cover
|
plugwise/common.py
CHANGED
@@ -27,7 +27,9 @@ from defusedxml import ElementTree as etree
|
|
27
27
|
from munch import Munch
|
28
28
|
|
29
29
|
|
30
|
-
def get_zigbee_data(
|
30
|
+
def get_zigbee_data(
|
31
|
+
module: etree.Element, module_data: ModuleData, legacy: bool
|
32
|
+
) -> None:
|
31
33
|
"""Helper-function for _get_module_data()."""
|
32
34
|
if legacy:
|
33
35
|
# Stretches
|
@@ -49,13 +51,18 @@ class SmileCommon:
|
|
49
51
|
"""Init."""
|
50
52
|
self._cooling_present: bool
|
51
53
|
self._count: int
|
52
|
-
self._domain_objects: etree
|
54
|
+
self._domain_objects: etree.Element
|
53
55
|
self._heater_id: str = NONE
|
54
56
|
self._on_off_device: bool
|
55
57
|
self.gw_entities: dict[str, GwEntityData] = {}
|
56
58
|
self.smile_name: str
|
57
59
|
self.smile_type: str
|
58
60
|
|
61
|
+
@property
|
62
|
+
def heater_id(self) -> str:
|
63
|
+
"""Return the heater-id."""
|
64
|
+
return self._heater_id
|
65
|
+
|
59
66
|
def smile(self, name: str) -> bool:
|
60
67
|
"""Helper-function checking the smile-name."""
|
61
68
|
return self.smile_name == name
|
@@ -63,10 +70,10 @@ class SmileCommon:
|
|
63
70
|
def _appl_heater_central_info(
|
64
71
|
self,
|
65
72
|
appl: Munch,
|
66
|
-
xml_1: etree,
|
73
|
+
xml_1: etree.Element,
|
67
74
|
legacy: bool,
|
68
|
-
xml_2: etree = None,
|
69
|
-
xml_3: etree = None,
|
75
|
+
xml_2: etree.Element = None,
|
76
|
+
xml_3: etree.Element = None,
|
70
77
|
) -> Munch:
|
71
78
|
"""Helper-function for _appliance_info_finder()."""
|
72
79
|
# Find the valid heater_central
|
@@ -74,6 +81,9 @@ class SmileCommon:
|
|
74
81
|
xml_2 = return_valid(xml_2, self._domain_objects)
|
75
82
|
self._heater_id = check_heater_central(xml_2)
|
76
83
|
|
84
|
+
if self._heater_id == NONE:
|
85
|
+
return Munch() # pragma: no cover
|
86
|
+
|
77
87
|
# Info for On-Off device
|
78
88
|
if self._on_off_device:
|
79
89
|
appl.name = "OnOff" # pragma: no cover
|
@@ -101,7 +111,7 @@ class SmileCommon:
|
|
101
111
|
return appl
|
102
112
|
|
103
113
|
def _appl_thermostat_info(
|
104
|
-
self, appl: Munch, xml_1: etree, xml_2: etree = None
|
114
|
+
self, appl: Munch, xml_1: etree.Element, xml_2: etree.Element = None
|
105
115
|
) -> Munch:
|
106
116
|
"""Helper-function for _appliance_info_finder()."""
|
107
117
|
locator = "./logs/point_log[type='thermostat']/thermostat"
|
@@ -190,7 +200,7 @@ class SmileCommon:
|
|
190
200
|
return switch_groups
|
191
201
|
|
192
202
|
def _get_lock_state(
|
193
|
-
self, xml: etree, data: GwEntityData, stretch_v2: bool = False
|
203
|
+
self, xml: etree.Element, data: GwEntityData, stretch_v2: bool = False
|
194
204
|
) -> None:
|
195
205
|
"""Helper-function for _get_measurement_data().
|
196
206
|
|
@@ -209,9 +219,9 @@ class SmileCommon:
|
|
209
219
|
|
210
220
|
def _get_module_data(
|
211
221
|
self,
|
212
|
-
xml_1: etree,
|
222
|
+
xml_1: etree.Element,
|
213
223
|
locator: str,
|
214
|
-
xml_2: etree = None,
|
224
|
+
xml_2: etree.Element = None,
|
215
225
|
legacy: bool = False,
|
216
226
|
) -> ModuleData:
|
217
227
|
"""Helper-function for _energy_device_info_finder() and _appliance_info_finder().
|
plugwise/constants.py
CHANGED
@@ -393,17 +393,6 @@ ZONE_THERMOSTATS: Final[tuple[str, ...]] = (
|
|
393
393
|
)
|
394
394
|
|
395
395
|
|
396
|
-
class SmileProps(TypedDict, total=False):
|
397
|
-
"""The SmileProps Data class."""
|
398
|
-
|
399
|
-
cooling_present: bool
|
400
|
-
gateway_id: str
|
401
|
-
heater_id: str
|
402
|
-
item_count: int
|
403
|
-
reboot: bool
|
404
|
-
smile_name: str
|
405
|
-
|
406
|
-
|
407
396
|
class ModuleData(TypedDict):
|
408
397
|
"""The Module data class."""
|
409
398
|
|
plugwise/data.py
CHANGED
@@ -16,7 +16,6 @@ from plugwise.constants import (
|
|
16
16
|
OFF,
|
17
17
|
ActuatorData,
|
18
18
|
GwEntityData,
|
19
|
-
SmileProps,
|
20
19
|
)
|
21
20
|
from plugwise.helper import SmileHelper
|
22
21
|
from plugwise.util import remove_empty_platform_dicts
|
@@ -27,28 +26,19 @@ class SmileData(SmileHelper):
|
|
27
26
|
|
28
27
|
def __init__(self) -> None:
|
29
28
|
"""Init."""
|
30
|
-
|
29
|
+
super().__init__()
|
31
30
|
self._zones: dict[str, GwEntityData] = {}
|
32
|
-
SmileHelper.__init__(self)
|
33
31
|
|
34
32
|
def _all_entity_data(self) -> None:
|
35
33
|
"""Helper-function for get_all_gateway_entities().
|
36
34
|
|
37
|
-
Collect data for each entity and add to self.
|
35
|
+
Collect data for each entity and add to self.gw_entities.
|
38
36
|
"""
|
39
37
|
self._update_gw_entities()
|
40
38
|
if self.smile(ADAM):
|
41
39
|
self._update_zones()
|
42
40
|
self.gw_entities.update(self._zones)
|
43
41
|
|
44
|
-
self._smile_props["gateway_id"] = self._gateway_id
|
45
|
-
self._smile_props["item_count"] = self._count
|
46
|
-
self._smile_props["reboot"] = True
|
47
|
-
self._smile_props["smile_name"] = self.smile_name
|
48
|
-
if self._is_thermostat:
|
49
|
-
self._smile_props["heater_id"] = self._heater_id
|
50
|
-
self._smile_props["cooling_present"] = self._cooling_present
|
51
|
-
|
52
42
|
def _update_zones(self) -> None:
|
53
43
|
"""Helper-function for _all_entity_data() and async_update().
|
54
44
|
|
@@ -228,6 +218,7 @@ class SmileData(SmileHelper):
|
|
228
218
|
for msg in item.values():
|
229
219
|
if message in msg:
|
230
220
|
data["available"] = False
|
221
|
+
break
|
231
222
|
|
232
223
|
def _get_adam_data(self, entity: GwEntityData, data: GwEntityData) -> None:
|
233
224
|
"""Helper-function for _get_entity_data().
|
@@ -329,16 +320,14 @@ class SmileData(SmileHelper):
|
|
329
320
|
|
330
321
|
Also, replace NONE by OFF when none of the schedules are active.
|
331
322
|
"""
|
332
|
-
loc_schedule_states: dict[str, str] = {}
|
333
|
-
for schedule in schedules:
|
334
|
-
loc_schedule_states[schedule] = "off"
|
335
|
-
if schedule == selected and data["climate_mode"] == "auto":
|
336
|
-
loc_schedule_states[schedule] = "on"
|
337
|
-
self._schedule_old_states[location] = loc_schedule_states
|
338
|
-
|
339
323
|
all_off = True
|
340
|
-
|
341
|
-
|
324
|
+
self._schedule_old_states[location] = {}
|
325
|
+
for schedule in schedules:
|
326
|
+
active: bool = schedule == selected and data["climate_mode"] == "auto"
|
327
|
+
self._schedule_old_states[location][schedule] = "off"
|
328
|
+
if active:
|
329
|
+
self._schedule_old_states[location][schedule] = "on"
|
342
330
|
all_off = False
|
331
|
+
|
343
332
|
if all_off:
|
344
333
|
data["select_schedule"] = OFF
|
plugwise/helper.py
CHANGED
@@ -59,7 +59,9 @@ from munch import Munch
|
|
59
59
|
from packaging import version
|
60
60
|
|
61
61
|
|
62
|
-
def search_actuator_functionalities(
|
62
|
+
def search_actuator_functionalities(
|
63
|
+
appliance: etree.Element, actuator: str
|
64
|
+
) -> etree.Element | None:
|
63
65
|
"""Helper-function for finding the relevant actuator xml-structure."""
|
64
66
|
locator = f"./actuator_functionalities/{actuator}"
|
65
67
|
if (search := appliance.find(locator)) is not None:
|
@@ -73,6 +75,7 @@ class SmileHelper(SmileCommon):
|
|
73
75
|
|
74
76
|
def __init__(self) -> None:
|
75
77
|
"""Set the constructor for this class."""
|
78
|
+
super().__init__()
|
76
79
|
self._endpoint: str
|
77
80
|
self._elga: bool
|
78
81
|
self._is_thermostat: bool
|
@@ -87,7 +90,16 @@ class SmileHelper(SmileCommon):
|
|
87
90
|
self.smile_model: str
|
88
91
|
self.smile_model_id: str | None
|
89
92
|
self.smile_version: version.Version
|
90
|
-
|
93
|
+
|
94
|
+
@property
|
95
|
+
def gateway_id(self) -> str:
|
96
|
+
"""Return the gateway-id."""
|
97
|
+
return self._gateway_id
|
98
|
+
|
99
|
+
@property
|
100
|
+
def item_count(self) -> int:
|
101
|
+
"""Return the item-count."""
|
102
|
+
return self._count
|
91
103
|
|
92
104
|
def _all_appliances(self) -> None:
|
93
105
|
"""Collect all appliances with relevant info.
|
@@ -195,6 +207,7 @@ class SmileHelper(SmileCommon):
|
|
195
207
|
other_entities = self.gw_entities
|
196
208
|
priority_entities = {entity_id: priority_entity}
|
197
209
|
self.gw_entities = {**priority_entities, **other_entities}
|
210
|
+
break
|
198
211
|
|
199
212
|
def _all_locations(self) -> None:
|
200
213
|
"""Collect all locations."""
|
@@ -212,7 +225,7 @@ class SmileHelper(SmileCommon):
|
|
212
225
|
f"./location[@id='{loc.loc_id}']"
|
213
226
|
)
|
214
227
|
|
215
|
-
def _appliance_info_finder(self, appl: Munch, appliance: etree) -> Munch:
|
228
|
+
def _appliance_info_finder(self, appl: Munch, appliance: etree.Element) -> Munch:
|
216
229
|
"""Collect info for all appliances found."""
|
217
230
|
match appl.pwclass:
|
218
231
|
case "gateway":
|
@@ -230,7 +243,7 @@ class SmileHelper(SmileCommon):
|
|
230
243
|
appliance, "domestic_hot_water_mode_control_functionality"
|
231
244
|
)
|
232
245
|
# Skip orphaned heater_central (Core Issue #104433)
|
233
|
-
if appl.entity_id != self.
|
246
|
+
if appl.entity_id != self.heater_id:
|
234
247
|
return Munch()
|
235
248
|
return appl
|
236
249
|
case _ as s if s.endswith("_plug"):
|
@@ -250,9 +263,9 @@ class SmileHelper(SmileCommon):
|
|
250
263
|
appl.zigbee_mac = module_data["zigbee_mac_address"]
|
251
264
|
return appl
|
252
265
|
case _: # pragma: no cover
|
253
|
-
return
|
266
|
+
return Munch()
|
254
267
|
|
255
|
-
def _appl_gateway_info(self, appl: Munch, appliance: etree) -> Munch:
|
268
|
+
def _appl_gateway_info(self, appl: Munch, appliance: etree.Element) -> Munch:
|
256
269
|
"""Helper-function for _appliance_info_finder()."""
|
257
270
|
self._gateway_id = appliance.attrib["id"]
|
258
271
|
appl.firmware = str(self.smile_version)
|
@@ -285,7 +298,7 @@ class SmileHelper(SmileCommon):
|
|
285
298
|
return appl
|
286
299
|
|
287
300
|
def _get_appl_actuator_modes(
|
288
|
-
self, appliance: etree, actuator_type: str
|
301
|
+
self, appliance: etree.Element, actuator_type: str
|
289
302
|
) -> list[str]:
|
290
303
|
"""Get allowed modes for the given actuator type."""
|
291
304
|
mode_list: list[str] = []
|
@@ -341,7 +354,7 @@ class SmileHelper(SmileCommon):
|
|
341
354
|
|
342
355
|
# Get non-P1 data from APPLIANCES
|
343
356
|
measurements = DEVICE_MEASUREMENTS
|
344
|
-
if self._is_thermostat and entity_id == self.
|
357
|
+
if self._is_thermostat and entity_id == self.heater_id:
|
345
358
|
measurements = HEATER_CENTRAL_MEASUREMENTS
|
346
359
|
# Show the allowed dhw_modes (Loria only)
|
347
360
|
if self._dhw_allowed_modes:
|
@@ -397,7 +410,7 @@ class SmileHelper(SmileCommon):
|
|
397
410
|
|
398
411
|
def _appliance_measurements(
|
399
412
|
self,
|
400
|
-
appliance: etree,
|
413
|
+
appliance: etree.Element,
|
401
414
|
data: GwEntityData,
|
402
415
|
measurements: dict[str, DATA | UOM],
|
403
416
|
) -> None:
|
@@ -429,7 +442,7 @@ class SmileHelper(SmileCommon):
|
|
429
442
|
self._count = count_data_items(self._count, data)
|
430
443
|
|
431
444
|
def _get_toggle_state(
|
432
|
-
self, xml: etree, toggle: str, name: ToggleNameType, data: GwEntityData
|
445
|
+
self, xml: etree.Element, toggle: str, name: ToggleNameType, data: GwEntityData
|
433
446
|
) -> None:
|
434
447
|
"""Helper-function for _get_measurement_data().
|
435
448
|
|
@@ -458,7 +471,7 @@ class SmileHelper(SmileCommon):
|
|
458
471
|
)
|
459
472
|
|
460
473
|
def _get_actuator_functionalities(
|
461
|
-
self, xml: etree, entity: GwEntityData, data: GwEntityData
|
474
|
+
self, xml: etree.Element, entity: GwEntityData, data: GwEntityData
|
462
475
|
) -> None:
|
463
476
|
"""Get and process the actuator_functionalities details for an entity.
|
464
477
|
|
@@ -520,7 +533,7 @@ class SmileHelper(SmileCommon):
|
|
520
533
|
data[act_item] = temp_dict
|
521
534
|
|
522
535
|
def _get_actuator_mode(
|
523
|
-
self, appliance: etree, entity_id: str, key: str
|
536
|
+
self, appliance: etree.Element, entity_id: str, key: str
|
524
537
|
) -> str | None:
|
525
538
|
"""Helper-function for _get_regulation_mode and _get_gateway_mode.
|
526
539
|
|
@@ -535,7 +548,7 @@ class SmileHelper(SmileCommon):
|
|
535
548
|
return None
|
536
549
|
|
537
550
|
def _get_regulation_mode(
|
538
|
-
self, appliance: etree, entity_id: str, data: GwEntityData
|
551
|
+
self, appliance: etree.Element, entity_id: str, data: GwEntityData
|
539
552
|
) -> None:
|
540
553
|
"""Helper-function for _get_measurement_data().
|
541
554
|
|
@@ -551,7 +564,7 @@ class SmileHelper(SmileCommon):
|
|
551
564
|
self._cooling_enabled = mode == "cooling"
|
552
565
|
|
553
566
|
def _get_gateway_mode(
|
554
|
-
self, appliance: etree, entity_id: str, data: GwEntityData
|
567
|
+
self, appliance: etree.Element, entity_id: str, data: GwEntityData
|
555
568
|
) -> None:
|
556
569
|
"""Helper-function for _get_measurement_data().
|
557
570
|
|
@@ -616,7 +629,7 @@ class SmileHelper(SmileCommon):
|
|
616
629
|
|
617
630
|
Support added for Techneco Elga and Thercon Loria/Thermastage.
|
618
631
|
"""
|
619
|
-
if entity_id != self.
|
632
|
+
if entity_id != self.heater_id:
|
620
633
|
return
|
621
634
|
|
622
635
|
if "elga_status_code" in data:
|
@@ -837,9 +850,7 @@ class SmileHelper(SmileCommon):
|
|
837
850
|
return presets # pragma: no cover
|
838
851
|
|
839
852
|
for rule_id in rule_ids:
|
840
|
-
directives
|
841
|
-
f'rule[@id="{rule_id}"]/directives'
|
842
|
-
)
|
853
|
+
directives = self._domain_objects.find(f'rule[@id="{rule_id}"]/directives')
|
843
854
|
for directive in directives:
|
844
855
|
preset = directive.find("then").attrib
|
845
856
|
presets[directive.attrib["preset"]] = [
|
plugwise/legacy/data.py
CHANGED
@@ -7,7 +7,7 @@ from __future__ import annotations
|
|
7
7
|
|
8
8
|
# Dict as class
|
9
9
|
# Version detection
|
10
|
-
from plugwise.constants import NONE, OFF, GwEntityData
|
10
|
+
from plugwise.constants import NONE, OFF, GwEntityData
|
11
11
|
from plugwise.legacy.helper import SmileLegacyHelper
|
12
12
|
from plugwise.util import remove_empty_platform_dicts
|
13
13
|
|
@@ -15,22 +15,12 @@ from plugwise.util import remove_empty_platform_dicts
|
|
15
15
|
class SmileLegacyData(SmileLegacyHelper):
|
16
16
|
"""The Plugwise Smile main class."""
|
17
17
|
|
18
|
-
def __init__(self) -> None:
|
19
|
-
"""Init."""
|
20
|
-
self._smile_props: SmileProps
|
21
|
-
SmileLegacyHelper.__init__(self)
|
22
|
-
|
23
18
|
def _all_entity_data(self) -> None:
|
24
19
|
"""Helper-function for get_all_gateway_entities().
|
25
20
|
|
26
|
-
Collect data for each entity and add to self.
|
21
|
+
Collect data for each entity and add to self.gw_entities.
|
27
22
|
"""
|
28
23
|
self._update_gw_entities()
|
29
|
-
self._smile_props["gateway_id"] = self.gateway_id
|
30
|
-
self._smile_props["item_count"] = self._count
|
31
|
-
self._smile_props["smile_name"] = self.smile_name
|
32
|
-
if self._is_thermostat:
|
33
|
-
self._smile_props["heater_id"] = self._heater_id
|
34
24
|
|
35
25
|
def _update_gw_entities(self) -> None:
|
36
26
|
"""Helper-function for _all_entity_data() and async_update().
|
plugwise/legacy/helper.py
CHANGED
@@ -23,6 +23,7 @@ from plugwise.constants import (
|
|
23
23
|
NONE,
|
24
24
|
OFF,
|
25
25
|
P1_LEGACY_MEASUREMENTS,
|
26
|
+
PRIORITY_DEVICE_CLASSES,
|
26
27
|
TEMP_CELSIUS,
|
27
28
|
THERMOSTAT_CLASSES,
|
28
29
|
UOM,
|
@@ -49,7 +50,7 @@ from munch import Munch
|
|
49
50
|
from packaging.version import Version
|
50
51
|
|
51
52
|
|
52
|
-
def etree_to_dict(element: etree) -> dict[str, str]:
|
53
|
+
def etree_to_dict(element: etree.Element) -> dict[str, str]:
|
53
54
|
"""Helper-function translating xml Element to dict."""
|
54
55
|
node: dict[str, str] = {}
|
55
56
|
if element is not None:
|
@@ -63,18 +64,29 @@ class SmileLegacyHelper(SmileCommon):
|
|
63
64
|
|
64
65
|
def __init__(self) -> None:
|
65
66
|
"""Set the constructor for this class."""
|
66
|
-
|
67
|
+
super().__init__()
|
68
|
+
self._appliances: etree.Element
|
69
|
+
self._gateway_id: str = NONE
|
67
70
|
self._is_thermostat: bool
|
68
71
|
self._loc_data: dict[str, ThermoLoc]
|
69
|
-
self._locations: etree
|
70
|
-
self._modules: etree
|
72
|
+
self._locations: etree.Element
|
73
|
+
self._modules: etree.Element
|
71
74
|
self._stretch_v2: bool
|
72
75
|
self.gw_entities: dict[str, GwEntityData] = {}
|
73
76
|
self.smile_mac_address: str | None
|
74
77
|
self.smile_model: str
|
75
78
|
self.smile_version: Version
|
76
79
|
self.smile_zigbee_mac_address: str | None
|
77
|
-
|
80
|
+
|
81
|
+
@property
|
82
|
+
def gateway_id(self) -> str:
|
83
|
+
"""Return the gateway-id."""
|
84
|
+
return self._gateway_id
|
85
|
+
|
86
|
+
@property
|
87
|
+
def item_count(self) -> int:
|
88
|
+
"""Return the item-count."""
|
89
|
+
return self._count
|
78
90
|
|
79
91
|
def _all_appliances(self) -> None:
|
80
92
|
"""Collect all appliances with relevant info."""
|
@@ -124,13 +136,13 @@ class SmileLegacyHelper(SmileCommon):
|
|
124
136
|
continue
|
125
137
|
|
126
138
|
# Skip orphaned heater_central (Core Issue #104433)
|
127
|
-
if appl.pwclass == "heater_central" and appl.entity_id != self.
|
139
|
+
if appl.pwclass == "heater_central" and appl.entity_id != self.heater_id:
|
128
140
|
continue # pragma: no cover
|
129
141
|
|
130
142
|
self._create_gw_entities(appl)
|
131
143
|
|
132
144
|
# Place the gateway and optional heater_central devices as 1st and 2nd
|
133
|
-
for dev_class in
|
145
|
+
for dev_class in PRIORITY_DEVICE_CLASSES:
|
134
146
|
for entity_id, entity in dict(self.gw_entities).items():
|
135
147
|
if entity["dev_class"] == dev_class:
|
136
148
|
tmp_entity = entity
|
@@ -138,6 +150,7 @@ class SmileLegacyHelper(SmileCommon):
|
|
138
150
|
cleared_dict = self.gw_entities
|
139
151
|
add_to_front = {entity_id: tmp_entity}
|
140
152
|
self.gw_entities = {**add_to_front, **cleared_dict}
|
153
|
+
break
|
141
154
|
|
142
155
|
def _all_locations(self) -> None:
|
143
156
|
"""Collect all locations."""
|
@@ -171,11 +184,11 @@ class SmileLegacyHelper(SmileCommon):
|
|
171
184
|
|
172
185
|
Use the home_location or FAKE_APPL as entity id.
|
173
186
|
"""
|
174
|
-
self.
|
187
|
+
self._gateway_id = self._home_loc_id
|
175
188
|
if self.smile_type == "power":
|
176
|
-
self.
|
189
|
+
self._gateway_id = FAKE_APPL
|
177
190
|
|
178
|
-
self.gw_entities[self.
|
191
|
+
self.gw_entities[self._gateway_id] = {"dev_class": "gateway"}
|
179
192
|
self._count += 1
|
180
193
|
for key, value in {
|
181
194
|
"firmware": str(self.smile_version),
|
@@ -188,7 +201,7 @@ class SmileLegacyHelper(SmileCommon):
|
|
188
201
|
}.items():
|
189
202
|
if value is not None:
|
190
203
|
gw_key = cast(ApplianceType, key)
|
191
|
-
self.gw_entities[self.
|
204
|
+
self.gw_entities[self._gateway_id][gw_key] = value
|
192
205
|
self._count += 1
|
193
206
|
|
194
207
|
def _appliance_info_finder(self, appliance: etree, appl: Munch) -> Munch:
|
@@ -266,7 +279,7 @@ class SmileLegacyHelper(SmileCommon):
|
|
266
279
|
return data
|
267
280
|
|
268
281
|
measurements = DEVICE_MEASUREMENTS
|
269
|
-
if self._is_thermostat and entity_id == self.
|
282
|
+
if self._is_thermostat and entity_id == self.heater_id:
|
270
283
|
measurements = HEATER_CENTRAL_MEASUREMENTS
|
271
284
|
|
272
285
|
if (
|
@@ -280,7 +293,7 @@ class SmileLegacyHelper(SmileCommon):
|
|
280
293
|
|
281
294
|
# Anna: the Smile outdoor_temperature is present in the Home location
|
282
295
|
# For some Anna's LOCATIONS is empty, falling back to domain_objects!
|
283
|
-
if self._is_thermostat and entity_id == self.
|
296
|
+
if self._is_thermostat and entity_id == self._gateway_id:
|
284
297
|
locator = f"./location[@id='{self._home_loc_id}']/logs/point_log[type='outdoor_temperature']/period/measurement"
|
285
298
|
if (found := self._domain_objects.find(locator)) is not None:
|
286
299
|
value = format_measure(found.text, NONE)
|
@@ -316,7 +329,7 @@ class SmileLegacyHelper(SmileCommon):
|
|
316
329
|
|
317
330
|
def _appliance_measurements(
|
318
331
|
self,
|
319
|
-
appliance: etree,
|
332
|
+
appliance: etree.Element,
|
320
333
|
data: GwEntityData,
|
321
334
|
measurements: dict[str, DATA | UOM],
|
322
335
|
) -> None:
|
@@ -345,7 +358,7 @@ class SmileLegacyHelper(SmileCommon):
|
|
345
358
|
self._count = count_data_items(self._count, data)
|
346
359
|
|
347
360
|
def _get_actuator_functionalities(
|
348
|
-
self, xml: etree, entity: GwEntityData, data: GwEntityData
|
361
|
+
self, xml: etree.Element, entity: GwEntityData, data: GwEntityData
|
349
362
|
) -> None:
|
350
363
|
"""Helper-function for _get_measurement_data()."""
|
351
364
|
for item in ACTIVE_ACTUATORS:
|
plugwise/legacy/smile.py
CHANGED
@@ -19,7 +19,6 @@ from plugwise.constants import (
|
|
19
19
|
REQUIRE_APPLIANCES,
|
20
20
|
RULES,
|
21
21
|
GwEntityData,
|
22
|
-
SmileProps,
|
23
22
|
ThermoLoc,
|
24
23
|
)
|
25
24
|
from plugwise.exceptions import ConnectionFailedError, DataMissingError, PlugwiseError
|
@@ -41,7 +40,6 @@ class SmileLegacyAPI(SmileLegacyData):
|
|
41
40
|
_on_off_device: bool,
|
42
41
|
_opentherm_device: bool,
|
43
42
|
_request: Callable[..., Awaitable[Any]],
|
44
|
-
_smile_props: SmileProps,
|
45
43
|
_stretch_v2: bool,
|
46
44
|
_target_smile: str,
|
47
45
|
smile_hostname: str,
|
@@ -54,13 +52,13 @@ class SmileLegacyAPI(SmileLegacyData):
|
|
54
52
|
smile_zigbee_mac_address: str | None,
|
55
53
|
) -> None:
|
56
54
|
"""Set the constructor for this class."""
|
55
|
+
super().__init__()
|
57
56
|
self._cooling_present = False
|
58
57
|
self._is_thermostat = _is_thermostat
|
59
58
|
self._loc_data = _loc_data
|
60
59
|
self._on_off_device = _on_off_device
|
61
60
|
self._opentherm_device = _opentherm_device
|
62
61
|
self._request = _request
|
63
|
-
self._smile_props = _smile_props
|
64
62
|
self._stretch_v2 = _stretch_v2
|
65
63
|
self._target_smile = _target_smile
|
66
64
|
self.smile_hostname = smile_hostname
|
@@ -71,11 +69,15 @@ class SmileLegacyAPI(SmileLegacyData):
|
|
71
69
|
self.smile_type = smile_type
|
72
70
|
self.smile_version = smile_version
|
73
71
|
self.smile_zigbee_mac_address = smile_zigbee_mac_address
|
74
|
-
SmileLegacyData.__init__(self)
|
75
72
|
|
76
73
|
self._first_update = True
|
77
74
|
self._previous_day_number: str = "0"
|
78
75
|
|
76
|
+
@property
|
77
|
+
def cooling_present(self) -> bool:
|
78
|
+
"""Return the cooling capability."""
|
79
|
+
return False
|
80
|
+
|
79
81
|
async def full_xml_update(self) -> None:
|
80
82
|
"""Perform a first fetch of the Plugwise server XML data."""
|
81
83
|
self._domain_objects = await self._request(DOMAIN_OBJECTS)
|
@@ -171,9 +173,8 @@ class SmileLegacyAPI(SmileLegacyData):
|
|
171
173
|
raise PlugwiseError("Plugwise: invalid preset.")
|
172
174
|
|
173
175
|
locator = f'rule/directives/when/then[@icon="{preset}"].../.../...'
|
174
|
-
|
175
|
-
data = f
|
176
|
-
|
176
|
+
rule_id = self._domain_objects.find(locator).attrib["id"]
|
177
|
+
data = f"<rules><rule id='{rule_id}'><active>true</active></rule></rules>"
|
177
178
|
await self.call_request(RULES, method="put", data=data)
|
178
179
|
|
179
180
|
async def set_regulation_mode(self, mode: str) -> None:
|
@@ -205,6 +206,7 @@ class SmileLegacyAPI(SmileLegacyData):
|
|
205
206
|
for rule in self._domain_objects.findall("rule"):
|
206
207
|
if rule.find("name").text == name:
|
207
208
|
schedule_rule_id = rule.attrib["id"]
|
209
|
+
break
|
208
210
|
|
209
211
|
if schedule_rule_id is None:
|
210
212
|
raise PlugwiseError(
|
@@ -216,36 +218,68 @@ class SmileLegacyAPI(SmileLegacyData):
|
|
216
218
|
new_state = "true"
|
217
219
|
|
218
220
|
locator = f'.//*[@id="{schedule_rule_id}"]/template'
|
219
|
-
|
220
|
-
template_id = rule.attrib["id"]
|
221
|
+
template_id = self._domain_objects.find(locator).attrib["id"]
|
221
222
|
|
222
|
-
uri = f"{RULES};id={schedule_rule_id}"
|
223
223
|
data = (
|
224
|
-
"<rules
|
225
|
-
f
|
226
|
-
f
|
224
|
+
"<rules>"
|
225
|
+
f"<rule id='{schedule_rule_id}'>"
|
226
|
+
f"<name><![CDATA[{name}]]></name>"
|
227
|
+
f"<template id='{template_id}' />"
|
228
|
+
f"<active>{new_state}</active>"
|
229
|
+
"</rule>"
|
230
|
+
"</rules>"
|
227
231
|
)
|
228
|
-
|
232
|
+
uri = f"{RULES};id={schedule_rule_id}"
|
229
233
|
await self.call_request(uri, method="put", data=data)
|
230
234
|
|
231
235
|
async def set_switch_state(
|
232
236
|
self, appl_id: str, members: list[str] | None, model: str, state: str
|
233
237
|
) -> None:
|
234
|
-
"""Set the given
|
238
|
+
"""Set the given state of the relevant switch.
|
239
|
+
|
240
|
+
For individual switches, sets the state directly.
|
241
|
+
For group switches, sets the state for each member in the group separately.
|
242
|
+
For switch-locks, sets the lock state using a different data format.
|
243
|
+
"""
|
235
244
|
switch = Munch()
|
236
245
|
switch.actuator = "actuator_functionalities"
|
237
246
|
switch.func_type = "relay_functionality"
|
238
247
|
if self._stretch_v2:
|
239
248
|
switch.actuator = "actuators"
|
240
249
|
switch.func_type = "relay"
|
241
|
-
switch.func = "state"
|
242
250
|
|
243
|
-
|
244
|
-
|
251
|
+
# Handle switch-lock
|
252
|
+
if model == "lock":
|
253
|
+
state = "false" if state == "off" else "true"
|
254
|
+
appliance = self._appliances.find(f'appliance[@id="{appl_id}"]')
|
255
|
+
appl_name = appliance.find("name").text
|
256
|
+
appl_type = appliance.find("type").text
|
257
|
+
data = (
|
258
|
+
"<appliances>"
|
259
|
+
f"<appliance id='{appl_id}'>"
|
260
|
+
f"<name><![CDATA[{appl_name}]]></name>"
|
261
|
+
f"<description><![CDATA[]]></description>"
|
262
|
+
f"<type><![CDATA[{appl_type}]]></type>"
|
263
|
+
f"<{switch.actuator}>"
|
264
|
+
f"<{switch.func_type}>"
|
265
|
+
f"<lock>{state}</lock>"
|
266
|
+
f"</{switch.func_type}>"
|
267
|
+
f"</{switch.actuator}>"
|
268
|
+
"</appliance>"
|
269
|
+
"</appliances>"
|
270
|
+
)
|
271
|
+
await self.call_request(APPLIANCES, method="post", data=data)
|
272
|
+
return
|
245
273
|
|
246
|
-
|
247
|
-
|
274
|
+
# Handle group of switches
|
275
|
+
data = f"<{switch.func_type}><state>{state}</state></{switch.func_type}>"
|
276
|
+
if members is not None:
|
277
|
+
return await self._set_groupswitch_member_state(
|
278
|
+
data, members, state, switch
|
279
|
+
)
|
248
280
|
|
281
|
+
# Handle individual relay switches
|
282
|
+
uri = f"{APPLIANCES};id={appl_id}/relay"
|
249
283
|
if model == "relay":
|
250
284
|
locator = (
|
251
285
|
f'appliance[@id="{appl_id}"]/{switch.actuator}/{switch.func_type}/lock'
|
@@ -257,16 +291,14 @@ class SmileLegacyAPI(SmileLegacyData):
|
|
257
291
|
await self.call_request(uri, method="put", data=data)
|
258
292
|
|
259
293
|
async def _set_groupswitch_member_state(
|
260
|
-
self, members: list[str], state: str, switch: Munch
|
294
|
+
self, data: str, members: list[str], state: str, switch: Munch
|
261
295
|
) -> None:
|
262
296
|
"""Helper-function for set_switch_state().
|
263
297
|
|
264
|
-
Set the given State of the relevant Switch within a group of members.
|
298
|
+
Set the given State of the relevant Switch (relay) within a group of members.
|
265
299
|
"""
|
266
300
|
for member in members:
|
267
|
-
uri = f"{APPLIANCES};id={member}/
|
268
|
-
data = f"<{switch.func_type}><{switch.func}>{state}</{switch.func}></{switch.func_type}>"
|
269
|
-
|
301
|
+
uri = f"{APPLIANCES};id={member}/relay"
|
270
302
|
await self.call_request(uri, method="put", data=data)
|
271
303
|
|
272
304
|
async def set_temperature(self, _: str, items: dict[str, float]) -> None:
|
@@ -281,12 +313,12 @@ class SmileLegacyAPI(SmileLegacyData):
|
|
281
313
|
) # pragma: no cover"
|
282
314
|
|
283
315
|
temperature = str(setpoint)
|
284
|
-
uri = self._thermostat_uri()
|
285
316
|
data = (
|
286
|
-
"<thermostat_functionality
|
287
|
-
f"{temperature}</setpoint
|
317
|
+
"<thermostat_functionality>"
|
318
|
+
f"<setpoint>{temperature}</setpoint>"
|
319
|
+
"</thermostat_functionality>"
|
288
320
|
)
|
289
|
-
|
321
|
+
uri = self._thermostat_uri()
|
290
322
|
await self.call_request(uri, method="put", data=data)
|
291
323
|
|
292
324
|
async def call_request(self, uri: str, **kwargs: Any) -> None:
|
plugwise/smile.py
CHANGED
@@ -18,11 +18,11 @@ from plugwise.constants import (
|
|
18
18
|
LOCATIONS,
|
19
19
|
MAX_SETPOINT,
|
20
20
|
MIN_SETPOINT,
|
21
|
+
NONE,
|
21
22
|
NOTIFICATIONS,
|
22
23
|
OFF,
|
23
24
|
RULES,
|
24
25
|
GwEntityData,
|
25
|
-
SmileProps,
|
26
26
|
ThermoLoc,
|
27
27
|
)
|
28
28
|
from plugwise.data import SmileData
|
@@ -51,7 +51,6 @@ class SmileAPI(SmileData):
|
|
51
51
|
_opentherm_device: bool,
|
52
52
|
_request: Callable[..., Awaitable[Any]],
|
53
53
|
_schedule_old_states: dict[str, dict[str, str]],
|
54
|
-
_smile_props: SmileProps,
|
55
54
|
smile_hostname: str | None,
|
56
55
|
smile_hw_version: str | None,
|
57
56
|
smile_mac_address: str | None,
|
@@ -62,6 +61,7 @@ class SmileAPI(SmileData):
|
|
62
61
|
smile_version: Version,
|
63
62
|
) -> None:
|
64
63
|
"""Set the constructor for this class."""
|
64
|
+
super().__init__()
|
65
65
|
self._cooling_present = _cooling_present
|
66
66
|
self._elga = _elga
|
67
67
|
self._is_thermostat = _is_thermostat
|
@@ -71,7 +71,6 @@ class SmileAPI(SmileData):
|
|
71
71
|
self._opentherm_device = _opentherm_device
|
72
72
|
self._request = _request
|
73
73
|
self._schedule_old_states = _schedule_old_states
|
74
|
-
self._smile_props = _smile_props
|
75
74
|
self.smile_hostname = smile_hostname
|
76
75
|
self.smile_hw_version = smile_hw_version
|
77
76
|
self.smile_mac_address = smile_mac_address
|
@@ -81,7 +80,11 @@ class SmileAPI(SmileData):
|
|
81
80
|
self.smile_type = smile_type
|
82
81
|
self.smile_version = smile_version
|
83
82
|
self.therms_with_offset_func: list[str] = []
|
84
|
-
|
83
|
+
|
84
|
+
@property
|
85
|
+
def cooling_present(self) -> bool:
|
86
|
+
"""Return the cooling capability."""
|
87
|
+
return self._cooling_present
|
85
88
|
|
86
89
|
async def full_xml_update(self) -> None:
|
87
90
|
"""Perform a first fetch of the Plugwise server XML data."""
|
@@ -121,8 +124,8 @@ class SmileAPI(SmileData):
|
|
121
124
|
self.get_all_gateway_entities()
|
122
125
|
# Set self._cooling_enabled - required for set_temperature(),
|
123
126
|
# also, check for a failed data-retrieval
|
124
|
-
if
|
125
|
-
heat_cooler = self.gw_entities[self.
|
127
|
+
if self.heater_id != NONE:
|
128
|
+
heat_cooler = self.gw_entities[self.heater_id]
|
126
129
|
if (
|
127
130
|
"binary_sensors" in heat_cooler
|
128
131
|
and "cooling_enabled" in heat_cooler["binary_sensors"]
|
@@ -131,7 +134,7 @@ class SmileAPI(SmileData):
|
|
131
134
|
"cooling_enabled"
|
132
135
|
]
|
133
136
|
else: # cover failed data-retrieval for P1
|
134
|
-
_ = self.gw_entities[self.
|
137
|
+
_ = self.gw_entities[self.gateway_id]["location"]
|
135
138
|
except KeyError as err:
|
136
139
|
raise DataMissingError("No Plugwise actual data received") from err
|
137
140
|
|
@@ -174,8 +177,12 @@ class SmileAPI(SmileData):
|
|
174
177
|
if thermostat_id is None:
|
175
178
|
raise PlugwiseError(f"Plugwise: cannot change setpoint, {key} not found.")
|
176
179
|
|
180
|
+
data = (
|
181
|
+
"<thermostat_functionality>"
|
182
|
+
f"<setpoint>{temp}</setpoint>"
|
183
|
+
"</thermostat_functionality>"
|
184
|
+
)
|
177
185
|
uri = f"{APPLIANCES};id={self._heater_id}/thermostat;id={thermostat_id}"
|
178
|
-
data = f"<thermostat_functionality><setpoint>{temp}</setpoint></thermostat_functionality>"
|
179
186
|
await self.call_request(uri, method="put", data=data)
|
180
187
|
|
181
188
|
async def set_offset(self, dev_id: str, offset: float) -> None:
|
@@ -186,9 +193,8 @@ class SmileAPI(SmileData):
|
|
186
193
|
)
|
187
194
|
|
188
195
|
value = str(offset)
|
189
|
-
uri = f"{APPLIANCES};id={dev_id}/offset;type=temperature_offset"
|
190
196
|
data = f"<offset_functionality><offset>{value}</offset></offset_functionality>"
|
191
|
-
|
197
|
+
uri = f"{APPLIANCES};id={dev_id}/offset;type=temperature_offset"
|
192
198
|
await self.call_request(uri, method="put", data=data)
|
193
199
|
|
194
200
|
async def set_preset(self, loc_id: str, preset: str) -> None:
|
@@ -201,14 +207,16 @@ class SmileAPI(SmileData):
|
|
201
207
|
current_location = self._domain_objects.find(f'location[@id="{loc_id}"]')
|
202
208
|
location_name = current_location.find("name").text
|
203
209
|
location_type = current_location.find("type").text
|
204
|
-
|
205
|
-
uri = f"{LOCATIONS};id={loc_id}"
|
206
210
|
data = (
|
207
|
-
"<locations
|
208
|
-
f' id="{loc_id}"
|
209
|
-
f"
|
211
|
+
"<locations>"
|
212
|
+
f'<location id="{loc_id}">'
|
213
|
+
f"<name>{location_name}</name>"
|
214
|
+
f"<type>{location_type}</type>"
|
215
|
+
f"<preset>{preset}</preset>"
|
216
|
+
"</location>"
|
217
|
+
"</locations>"
|
210
218
|
)
|
211
|
-
|
219
|
+
uri = f"{LOCATIONS};id={loc_id}"
|
212
220
|
await self.call_request(uri, method="put", data=data)
|
213
221
|
|
214
222
|
async def set_select(
|
@@ -231,9 +239,12 @@ class SmileAPI(SmileData):
|
|
231
239
|
if mode not in self._dhw_allowed_modes:
|
232
240
|
raise PlugwiseError("Plugwise: invalid dhw mode.")
|
233
241
|
|
242
|
+
data = (
|
243
|
+
"<domestic_hot_water_mode_control_functionality>"
|
244
|
+
f"<mode>{mode}</mode>"
|
245
|
+
"</domestic_hot_water_mode_control_functionality>"
|
246
|
+
)
|
234
247
|
uri = f"{APPLIANCES};type=heater_central/domestic_hot_water_mode_control"
|
235
|
-
data = f"<domestic_hot_water_mode_control_functionality><mode>{mode}</mode></domestic_hot_water_mode_control_functionality>"
|
236
|
-
|
237
248
|
await self.call_request(uri, method="put", data=data)
|
238
249
|
|
239
250
|
async def set_gateway_mode(self, mode: str) -> None:
|
@@ -259,9 +270,13 @@ class SmileAPI(SmileData):
|
|
259
270
|
vacation_time = time_2 + "T23:00:00.000Z"
|
260
271
|
valid = f"<valid_from>{vacation_time}</valid_from><valid_to>{end_time}</valid_to>"
|
261
272
|
|
262
|
-
|
263
|
-
|
264
|
-
|
273
|
+
data = (
|
274
|
+
"<gateway_mode_control_functionality>"
|
275
|
+
f"<mode>{mode}</mode>"
|
276
|
+
f"{valid}"
|
277
|
+
"</gateway_mode_control_functionality>"
|
278
|
+
)
|
279
|
+
uri = f"{APPLIANCES};id={self.gateway_id}/gateway_mode_control"
|
265
280
|
await self.call_request(uri, method="put", data=data)
|
266
281
|
|
267
282
|
async def set_regulation_mode(self, mode: str) -> None:
|
@@ -269,12 +284,17 @@ class SmileAPI(SmileData):
|
|
269
284
|
if mode not in self._reg_allowed_modes:
|
270
285
|
raise PlugwiseError("Plugwise: invalid regulation mode.")
|
271
286
|
|
272
|
-
uri = f"{APPLIANCES};type=gateway/regulation_mode_control"
|
273
287
|
duration = ""
|
274
288
|
if "bleeding" in mode:
|
275
289
|
duration = "<duration>300</duration>"
|
276
|
-
data = f"<regulation_mode_control_functionality>{duration}<mode>{mode}</mode></regulation_mode_control_functionality>"
|
277
290
|
|
291
|
+
data = (
|
292
|
+
"<regulation_mode_control_functionality>"
|
293
|
+
f"{duration}"
|
294
|
+
f"<mode>{mode}</mode>"
|
295
|
+
"</regulation_mode_control_functionality>"
|
296
|
+
)
|
297
|
+
uri = f"{APPLIANCES};type=gateway/regulation_mode_control"
|
278
298
|
await self.call_request(uri, method="put", data=data)
|
279
299
|
|
280
300
|
async def set_schedule_state(
|
@@ -323,18 +343,22 @@ class SmileAPI(SmileData):
|
|
323
343
|
template = f'<template id="{template_id}" />'
|
324
344
|
|
325
345
|
contexts = self.determine_contexts(loc_id, name, new_state, schedule_rule_id)
|
326
|
-
uri = f"{RULES};id={schedule_rule_id}"
|
327
346
|
data = (
|
328
|
-
|
329
|
-
f"{
|
347
|
+
"<rules>"
|
348
|
+
f"<rule id='{schedule_rule_id}'>"
|
349
|
+
f"<name><![CDATA[{name}]]></name>"
|
350
|
+
f"{template}"
|
351
|
+
f"{contexts}"
|
352
|
+
"</rule>"
|
353
|
+
"</rules>"
|
330
354
|
)
|
331
|
-
|
355
|
+
uri = f"{RULES};id={schedule_rule_id}"
|
332
356
|
await self.call_request(uri, method="put", data=data)
|
333
357
|
self._schedule_old_states[loc_id][name] = new_state
|
334
358
|
|
335
359
|
def determine_contexts(
|
336
360
|
self, loc_id: str, name: str, state: str, sched_id: str
|
337
|
-
) ->
|
361
|
+
) -> str:
|
338
362
|
"""Helper-function for set_schedule_state()."""
|
339
363
|
locator = f'.//*[@id="{sched_id}"]/contexts'
|
340
364
|
contexts = self._domain_objects.find(locator)
|
@@ -349,7 +373,7 @@ class SmileAPI(SmileData):
|
|
349
373
|
if state == "on":
|
350
374
|
contexts.append(subject)
|
351
375
|
|
352
|
-
return etree.tostring(contexts, encoding="unicode").rstrip()
|
376
|
+
return str(etree.tostring(contexts, encoding="unicode").rstrip())
|
353
377
|
|
354
378
|
async def set_switch_state(
|
355
379
|
self, appl_id: str, members: list[str] | None, model: str, state: str
|
@@ -378,18 +402,22 @@ class SmileAPI(SmileData):
|
|
378
402
|
return await self._set_groupswitch_member_state(members, state, switch)
|
379
403
|
|
380
404
|
locator = f'appliance[@id="{appl_id}"]/{switch.actuator}/{switch.func_type}'
|
381
|
-
found
|
405
|
+
found = self._domain_objects.findall(locator)
|
382
406
|
for item in found:
|
407
|
+
# multiple types of e.g. toggle_functionality present
|
383
408
|
if (sw_type := item.find("type")) is not None:
|
384
409
|
if sw_type.text == switch.act_type:
|
385
410
|
switch_id = item.attrib["id"]
|
386
|
-
|
411
|
+
break
|
412
|
+
else: # actuators with a single item like relay_functionality
|
387
413
|
switch_id = item.attrib["id"]
|
388
|
-
break
|
389
414
|
|
415
|
+
data = (
|
416
|
+
f"<{switch.func_type}>"
|
417
|
+
f"<{switch.func}>{state}</{switch.func}>"
|
418
|
+
f"</{switch.func_type}>"
|
419
|
+
)
|
390
420
|
uri = f"{APPLIANCES};id={appl_id}/{switch.device};id={switch_id}"
|
391
|
-
data = f"<{switch.func_type}><{switch.func}>{state}</{switch.func}></{switch.func_type}>"
|
392
|
-
|
393
421
|
if model == "relay":
|
394
422
|
locator = (
|
395
423
|
f'appliance[@id="{appl_id}"]/{switch.actuator}/{switch.func_type}/lock'
|
@@ -411,8 +439,11 @@ class SmileAPI(SmileData):
|
|
411
439
|
locator = f'appliance[@id="{member}"]/{switch.actuator}/{switch.func_type}'
|
412
440
|
switch_id = self._domain_objects.find(locator).attrib["id"]
|
413
441
|
uri = f"{APPLIANCES};id={member}/{switch.device};id={switch_id}"
|
414
|
-
data =
|
415
|
-
|
442
|
+
data = (
|
443
|
+
f"<{switch.func_type}>"
|
444
|
+
f"<{switch.func}>{state}</{switch.func}>"
|
445
|
+
f"</{switch.func_type}>"
|
446
|
+
)
|
416
447
|
await self.call_request(uri, method="put", data=data)
|
417
448
|
|
418
449
|
async def set_temperature(self, loc_id: str, items: dict[str, float]) -> None:
|
@@ -448,12 +479,12 @@ class SmileAPI(SmileData):
|
|
448
479
|
) # pragma: no cover"
|
449
480
|
|
450
481
|
temperature = str(setpoint)
|
451
|
-
uri = self._thermostat_uri(loc_id)
|
452
482
|
data = (
|
453
|
-
"<thermostat_functionality
|
454
|
-
f"{temperature}</setpoint
|
483
|
+
"<thermostat_functionality>"
|
484
|
+
f"<setpoint>{temperature}</setpoint>"
|
485
|
+
"</thermostat_functionality>"
|
455
486
|
)
|
456
|
-
|
487
|
+
uri = self._thermostat_uri(loc_id)
|
457
488
|
await self.call_request(uri, method="put", data=data)
|
458
489
|
|
459
490
|
async def call_request(self, uri: str, **kwargs: Any) -> None:
|
plugwise/smilecomm.py
CHANGED
@@ -51,7 +51,7 @@ class SmileComm:
|
|
51
51
|
retry: int = 3,
|
52
52
|
method: str = "get",
|
53
53
|
data: str | None = None,
|
54
|
-
) -> etree:
|
54
|
+
) -> etree.Element:
|
55
55
|
"""Get/put/delete data from a give URL."""
|
56
56
|
resp: ClientResponse
|
57
57
|
url = f"{self._endpoint}{command}"
|
@@ -107,7 +107,9 @@ class SmileComm:
|
|
107
107
|
|
108
108
|
return await self._request_validate(resp, method)
|
109
109
|
|
110
|
-
async def _request_validate(
|
110
|
+
async def _request_validate(
|
111
|
+
self, resp: ClientResponse, method: str
|
112
|
+
) -> etree.Element:
|
111
113
|
"""Helper-function for _request(): validate the returned data."""
|
112
114
|
match resp.status:
|
113
115
|
case 200:
|
plugwise/util.py
CHANGED
@@ -13,6 +13,7 @@ from plugwise.constants import (
|
|
13
13
|
ELECTRIC_POTENTIAL_VOLT,
|
14
14
|
ENERGY_KILO_WATT_HOUR,
|
15
15
|
HW_MODELS,
|
16
|
+
NONE,
|
16
17
|
OBSOLETE_MEASUREMENTS,
|
17
18
|
PERCENTAGE,
|
18
19
|
POWER_WATT,
|
@@ -74,7 +75,7 @@ def in_alternative_location(loc: Munch, legacy: bool) -> bool:
|
|
74
75
|
return present
|
75
76
|
|
76
77
|
|
77
|
-
def check_heater_central(xml: etree) -> str:
|
78
|
+
def check_heater_central(xml: etree.Element) -> str:
|
78
79
|
"""Find the valid heater_central, helper-function for _appliance_info_finder().
|
79
80
|
|
80
81
|
Solution for Core Issue #104433,
|
@@ -93,14 +94,16 @@ def check_heater_central(xml: etree) -> str:
|
|
93
94
|
if heater_central.find("name").text == "Central heating boiler":
|
94
95
|
hc_list.append({hc_id: has_actuators})
|
95
96
|
|
97
|
+
if not hc_list:
|
98
|
+
return NONE # pragma: no cover
|
99
|
+
|
96
100
|
heater_central_id = list(hc_list[0].keys())[0]
|
97
101
|
if hc_count > 1:
|
98
|
-
for item in hc_list:
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
break # pragma: no cover
|
102
|
+
for item in hc_list:
|
103
|
+
hc_id, has_actuators = next(iter(item.items()))
|
104
|
+
if has_actuators:
|
105
|
+
heater_central_id = hc_id
|
106
|
+
break
|
104
107
|
|
105
108
|
return heater_central_id
|
106
109
|
|
@@ -135,7 +138,7 @@ def collect_power_values(
|
|
135
138
|
if not loc.found:
|
136
139
|
continue
|
137
140
|
|
138
|
-
|
141
|
+
power_data_energy_diff(loc.measurement, loc.net_string, loc.f_val, data)
|
139
142
|
key = cast(SensorType, loc.key_string)
|
140
143
|
data["sensors"][key] = loc.f_val
|
141
144
|
|
@@ -143,7 +146,7 @@ def collect_power_values(
|
|
143
146
|
def common_match_cases(
|
144
147
|
measurement: str,
|
145
148
|
attrs: DATA | UOM,
|
146
|
-
location: etree,
|
149
|
+
location: etree.Element,
|
147
150
|
data: GwEntityData,
|
148
151
|
) -> None:
|
149
152
|
"""Helper-function for common match-case execution."""
|
@@ -192,8 +195,6 @@ def escape_illegal_xml_characters(xmldata: str) -> str:
|
|
192
195
|
|
193
196
|
def format_measure(measure: str, unit: str) -> float | int:
|
194
197
|
"""Format measure to correct type."""
|
195
|
-
result: float | int = 0
|
196
|
-
|
197
198
|
float_measure = float(measure)
|
198
199
|
if unit == PERCENTAGE and 0 < float_measure <= 1:
|
199
200
|
return int(float_measure * 100)
|
@@ -202,18 +203,18 @@ def format_measure(measure: str, unit: str) -> float | int:
|
|
202
203
|
float_measure = float_measure / 1000
|
203
204
|
|
204
205
|
if unit in SPECIAL_FORMAT:
|
205
|
-
result =
|
206
|
+
result = round(float_measure, 3)
|
206
207
|
elif unit == ELECTRIC_POTENTIAL_VOLT:
|
207
|
-
result =
|
208
|
+
result = round(float_measure, 1)
|
208
209
|
elif abs(float_measure) < 10:
|
209
|
-
result =
|
210
|
-
|
211
|
-
result =
|
210
|
+
result = round(float_measure, 2)
|
211
|
+
else: # abs(float_measure) >= 10
|
212
|
+
result = round(float_measure, 1)
|
212
213
|
|
213
214
|
return result
|
214
215
|
|
215
216
|
|
216
|
-
def get_vendor_name(module: etree, model_data: ModuleData) -> ModuleData:
|
217
|
+
def get_vendor_name(module: etree.Element, model_data: ModuleData) -> ModuleData:
|
217
218
|
"""Helper-function for _get_model_data()."""
|
218
219
|
if (vendor_name := module.find("vendor_name").text) is not None:
|
219
220
|
model_data["vendor_name"] = vendor_name
|
@@ -228,31 +229,21 @@ def power_data_energy_diff(
|
|
228
229
|
net_string: SensorType,
|
229
230
|
f_val: float | int,
|
230
231
|
data: GwEntityData,
|
231
|
-
) ->
|
232
|
+
) -> None:
|
232
233
|
"""Calculate differential energy."""
|
233
234
|
if (
|
234
235
|
"electricity" in measurement
|
235
236
|
and "phase" not in measurement
|
236
237
|
and "interval" not in net_string
|
237
238
|
):
|
238
|
-
diff = 1
|
239
|
-
|
240
|
-
|
241
|
-
if
|
242
|
-
tmp_val
|
243
|
-
else:
|
244
|
-
tmp_val = data["sensors"][net_string]
|
245
|
-
|
246
|
-
if isinstance(f_val, int):
|
247
|
-
tmp_val += f_val * diff
|
248
|
-
else:
|
249
|
-
tmp_val += float(f_val * diff)
|
250
|
-
tmp_val = float(f"{round(tmp_val, 3):.3f}")
|
239
|
+
diff = 1 if "consumed" in measurement else -1
|
240
|
+
tmp_val = data["sensors"].get(net_string, 0)
|
241
|
+
tmp_val += f_val * diff
|
242
|
+
if isinstance(f_val, float):
|
243
|
+
tmp_val = round(tmp_val, 3)
|
251
244
|
|
252
245
|
data["sensors"][net_string] = tmp_val
|
253
246
|
|
254
|
-
return data
|
255
|
-
|
256
247
|
|
257
248
|
def power_data_local_format(
|
258
249
|
attrs: dict[str, str], key_string: str, val: str
|
@@ -302,12 +293,12 @@ def remove_empty_platform_dicts(data: GwEntityData) -> None:
|
|
302
293
|
data.pop("switches")
|
303
294
|
|
304
295
|
|
305
|
-
def return_valid(value: etree | None, default: etree) -> etree:
|
296
|
+
def return_valid(value: etree.Element | None, default: etree.Element) -> etree.Element:
|
306
297
|
"""Return default when value is None."""
|
307
298
|
return value if value is not None else default
|
308
299
|
|
309
300
|
|
310
|
-
def skip_obsolete_measurements(xml: etree, measurement: str) -> bool:
|
301
|
+
def skip_obsolete_measurements(xml: etree.Element, measurement: str) -> bool:
|
311
302
|
"""Skipping known obsolete measurements."""
|
312
303
|
locator = f".//logs/point_log[type='{measurement}']/updated_date"
|
313
304
|
if (
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: plugwise
|
3
|
-
Version: 1.7.
|
3
|
+
Version: 1.7.3
|
4
4
|
Summary: Plugwise Smile (Adam/Anna/P1) and Stretch module for Python 3.
|
5
5
|
Home-page: https://github.com/plugwise/python-plugwise
|
6
6
|
Author: Plugwise device owners
|
@@ -48,12 +48,13 @@ Requires-Dist: python-dateutil
|
|
48
48
|
|
49
49
|
# Plugwise python module
|
50
50
|
|
51
|
-
This module is the backend for the [`plugwise` component](https://github.com/home-assistant/core/tree/dev/homeassistant/components/plugwise) in Home Assistant Core (which we maintain as
|
51
|
+
This module is the backend for the [`plugwise` component](https://github.com/home-assistant/core/tree/dev/homeassistant/components/plugwise) in Home Assistant Core (which we maintain as code owners).
|
52
52
|
|
53
|
-
This module supports `Smile`s
|
53
|
+
This module supports Hubs such as `Adam`, `Smile`s for Anna and P1 and `Stretch`, i.e. the networked plugwise devices. For the USB (or Stick-standalone version) please refer to upcoming [`plugwise-usb` component](https://github.com/plugwise/plugwise_usb-beta).
|
54
54
|
|
55
55
|
Our main usage for this module is supporting [Home Assistant](https://www.home-assistant.io) / [home-assistant](http://github.com/home-assistant/core/)
|
56
56
|
|
57
|
+

|
57
58
|
[](https://github.com/plugwise)
|
58
59
|
[](https://coderabbit.ai)
|
59
60
|
[](https://github.com/plugwise/python-plugwise/issues/291)
|
@@ -0,0 +1,18 @@
|
|
1
|
+
plugwise/__init__.py,sha256=BKT3BagtOOy2Efk2m8uQLFPE4vizEjjPMj61Zs7MwMw,17492
|
2
|
+
plugwise/common.py,sha256=_O7cC7fPGHZkrxQTaK2y8_trr9lOjEtgbS6plOfp2jk,9589
|
3
|
+
plugwise/constants.py,sha256=zJFm0J14PJWasKSvepriOc6mYLljRVGNGAF5QDtzyl0,16869
|
4
|
+
plugwise/data.py,sha256=OxZufaAmnyVDkRGJBdk6tf-I65FaCwcT69scNsZnmiA,12490
|
5
|
+
plugwise/exceptions.py,sha256=Ce-tO9uNsMB-8FP6VAxBvsHNJ-NIM9F0onUZOdZI4Ys,1110
|
6
|
+
plugwise/helper.py,sha256=AdQ4Tno5zheFI5y5A-YovVGYW_islxCxkt0B77TfC-o,39701
|
7
|
+
plugwise/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
|
+
plugwise/smile.py,sha256=WPZ0v45RpgJxBWEy_Sy_vsX87V4UPaVkkSgY7yZouAQ,19550
|
9
|
+
plugwise/smilecomm.py,sha256=DRQ3toRNEf3oo_mej49fPJ47m5das-jvo-8GnIrSPzw,5208
|
10
|
+
plugwise/util.py,sha256=rMcqfaB4dkQEZFJY-bBJISmlYgTnb6Ns3-Doxelf92Q,10689
|
11
|
+
plugwise/legacy/data.py,sha256=Z-7nw21s9-L4DcwPCZ_yoRGeI_fWBS1Q48kHmyh2pkY,3145
|
12
|
+
plugwise/legacy/helper.py,sha256=cDi8zvUtoCqxVrLksqjlCHUmjXjulXCf-l9fMjFPOfs,17417
|
13
|
+
plugwise/legacy/smile.py,sha256=Q133Ub5W6VB1MyxzAZze3cFyXvTxIeIbCxSUP4eXNXM,12867
|
14
|
+
plugwise-1.7.3.dist-info/LICENSE,sha256=mL22BjmXtg_wnoDnnaqps5_Bg_VGj_yHueX5lsKwbCc,1144
|
15
|
+
plugwise-1.7.3.dist-info/METADATA,sha256=5sR52dUprms7TDT3wI7zO5-87uRT_ZhCCz420kCz970,9328
|
16
|
+
plugwise-1.7.3.dist-info/WHEEL,sha256=nn6H5-ilmfVryoAQl3ZQ2l8SH5imPWFpm1A5FgEuFV4,91
|
17
|
+
plugwise-1.7.3.dist-info/top_level.txt,sha256=MYOmktMFf8ZmX6_OE1y9MoCZFfY-L8DA0F2tA2IvE4s,9
|
18
|
+
plugwise-1.7.3.dist-info/RECORD,,
|
plugwise-1.7.1.dist-info/RECORD
DELETED
@@ -1,18 +0,0 @@
|
|
1
|
-
plugwise/__init__.py,sha256=hLwUqdGAo39LzTXYz6Lkua_9RelJx5k-uAeJSrZdfYk,17760
|
2
|
-
plugwise/common.py,sha256=_41FLLjgccaHjaV0Ndn1YEkxjB_qKev1k3ZnhSUzXjc,9305
|
3
|
-
plugwise/constants.py,sha256=Vd8tvOHsRtZxtUUFXBXolZj3QiKnxRt0bnwDT9DoEqE,17073
|
4
|
-
plugwise/data.py,sha256=ITGnS5RiRbd3O2KbYLc9_5okw81zLl7azXNXtuTwaoY,13022
|
5
|
-
plugwise/exceptions.py,sha256=Ce-tO9uNsMB-8FP6VAxBvsHNJ-NIM9F0onUZOdZI4Ys,1110
|
6
|
-
plugwise/helper.py,sha256=v8qXFau2bXnJ4xGde9NuljA-LLEWSL1SJVcnXQX-jiE,39397
|
7
|
-
plugwise/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
|
-
plugwise/smile.py,sha256=J8_CevN7FSYnAccYv4Hp_H3o9lfHGM_fYlLYAvuqJKw,18921
|
9
|
-
plugwise/smilecomm.py,sha256=aQ2KkebDTou18k-flrVmTnFkgls33Xu8D9ZZQQt2R5k,5178
|
10
|
-
plugwise/util.py,sha256=d3nOS9fzZ1MwJX8_77KOgW3CtQnGYc0oGcnqHrlxlLo,11015
|
11
|
-
plugwise/legacy/data.py,sha256=s2WYjgxwcuAGS8UOxJVf7xLSxS38Zgr8GsjxlxfD98w,3574
|
12
|
-
plugwise/legacy/helper.py,sha256=CjLGUhWRgNCOyooVyylLABdv0H2zoiKzjhQ-g7E8x9M,17059
|
13
|
-
plugwise/legacy/smile.py,sha256=aCG5uaOCS4GOGfIbxTp8U-CFE3Cib-UspB4qSamm_eY,11659
|
14
|
-
plugwise-1.7.1.dist-info/LICENSE,sha256=mL22BjmXtg_wnoDnnaqps5_Bg_VGj_yHueX5lsKwbCc,1144
|
15
|
-
plugwise-1.7.1.dist-info/METADATA,sha256=Dnc7KdcTcDjT33JYXSkkDkHXkxKHXEet2gXQcQpSuyo,9148
|
16
|
-
plugwise-1.7.1.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
17
|
-
plugwise-1.7.1.dist-info/top_level.txt,sha256=MYOmktMFf8ZmX6_OE1y9MoCZFfY-L8DA0F2tA2IvE4s,9
|
18
|
-
plugwise-1.7.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|