plugwise 1.6.3__py3-none-any.whl → 1.7.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 +134 -100
- plugwise/common.py +32 -123
- plugwise/constants.py +6 -16
- plugwise/data.py +60 -37
- plugwise/helper.py +198 -333
- plugwise/legacy/data.py +20 -12
- plugwise/legacy/helper.py +31 -63
- plugwise/legacy/smile.py +64 -60
- plugwise/smile.py +50 -54
- plugwise/smilecomm.py +148 -0
- plugwise/util.py +117 -28
- {plugwise-1.6.3.dist-info → plugwise-1.7.0.dist-info}/METADATA +2 -2
- plugwise-1.7.0.dist-info/RECORD +18 -0
- {plugwise-1.6.3.dist-info → plugwise-1.7.0.dist-info}/WHEEL +1 -1
- plugwise-1.6.3.dist-info/RECORD +0 -17
- {plugwise-1.6.3.dist-info → plugwise-1.7.0.dist-info}/LICENSE +0 -0
- {plugwise-1.6.3.dist-info → plugwise-1.7.0.dist-info}/top_level.txt +0 -0
plugwise/constants.py
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
"""Plugwise Smile constants."""
|
2
|
+
|
2
3
|
from __future__ import annotations
|
3
4
|
|
4
5
|
from collections import namedtuple
|
5
|
-
from dataclasses import dataclass
|
6
6
|
import logging
|
7
7
|
from typing import Final, Literal, TypedDict, get_args
|
8
8
|
|
@@ -84,6 +84,7 @@ MIN_SETPOINT: Final[float] = 4.0
|
|
84
84
|
MODULE_LOCATOR: Final = "./logs/point_log/*[@id]"
|
85
85
|
NONE: Final = "None"
|
86
86
|
OFF: Final = "off"
|
87
|
+
PRIORITY_DEVICE_CLASSES = ("heater_central", "gateway")
|
87
88
|
|
88
89
|
# XML data paths
|
89
90
|
APPLIANCES: Final = "/core/appliances"
|
@@ -167,9 +168,7 @@ HEATER_CENTRAL_MEASUREMENTS: Final[dict[str, DATA | UOM]] = {
|
|
167
168
|
"intended_boiler_state": DATA(
|
168
169
|
"heating_state", NONE
|
169
170
|
), # Legacy Anna: shows when heating is active, we don't show dhw_state, cannot be determined reliably
|
170
|
-
"flame_state": UOM(
|
171
|
-
NONE
|
172
|
-
), # Also present when there is a single gas-heater
|
171
|
+
"flame_state": UOM(NONE), # Also present when there is a single gas-heater
|
173
172
|
"intended_boiler_temperature": UOM(
|
174
173
|
TEMP_CELSIUS
|
175
174
|
), # Non-zero when heating, zero when dhw-heating
|
@@ -394,14 +393,13 @@ ZONE_THERMOSTATS: Final[tuple[str, ...]] = (
|
|
394
393
|
)
|
395
394
|
|
396
395
|
|
397
|
-
class
|
398
|
-
"""The
|
396
|
+
class SmileProps(TypedDict, total=False):
|
397
|
+
"""The SmileProps Data class."""
|
399
398
|
|
400
399
|
cooling_present: bool
|
401
400
|
gateway_id: str
|
402
401
|
heater_id: str
|
403
402
|
item_count: int
|
404
|
-
notifications: dict[str, dict[str, str]]
|
405
403
|
reboot: bool
|
406
404
|
smile_name: str
|
407
405
|
|
@@ -552,6 +550,7 @@ class GwEntityData(TypedDict, total=False):
|
|
552
550
|
|
553
551
|
# Gateway
|
554
552
|
gateway_modes: list[str]
|
553
|
+
notifications: dict[str, dict[str, str]]
|
555
554
|
regulation_modes: list[str]
|
556
555
|
select_gateway_mode: str
|
557
556
|
select_regulation_mode: str
|
@@ -577,12 +576,3 @@ class GwEntityData(TypedDict, total=False):
|
|
577
576
|
switches: SmileSwitches
|
578
577
|
temperature_offset: ActuatorData
|
579
578
|
thermostat: ActuatorData
|
580
|
-
|
581
|
-
|
582
|
-
@dataclass
|
583
|
-
class PlugwiseData:
|
584
|
-
"""Plugwise data provided as output."""
|
585
|
-
|
586
|
-
devices: dict[str, GwEntityData]
|
587
|
-
gateway: GatewayData
|
588
|
-
|
plugwise/data.py
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
Plugwise Smile protocol data-collection helpers.
|
4
4
|
"""
|
5
|
+
|
5
6
|
from __future__ import annotations
|
6
7
|
|
7
8
|
import re
|
@@ -15,6 +16,7 @@ from plugwise.constants import (
|
|
15
16
|
OFF,
|
16
17
|
ActuatorData,
|
17
18
|
GwEntityData,
|
19
|
+
SmileProps,
|
18
20
|
)
|
19
21
|
from plugwise.helper import SmileHelper
|
20
22
|
from plugwise.util import remove_empty_platform_dicts
|
@@ -25,32 +27,27 @@ class SmileData(SmileHelper):
|
|
25
27
|
|
26
28
|
def __init__(self) -> None:
|
27
29
|
"""Init."""
|
30
|
+
self._smile_props: SmileProps
|
31
|
+
self._zones: dict[str, GwEntityData] = {}
|
28
32
|
SmileHelper.__init__(self)
|
29
33
|
|
30
|
-
|
31
34
|
def _all_entity_data(self) -> None:
|
32
35
|
"""Helper-function for get_all_gateway_entities().
|
33
36
|
|
34
|
-
Collect data for each entity and add to self.
|
37
|
+
Collect data for each entity and add to self._smile_props and self.gw_entities.
|
35
38
|
"""
|
36
39
|
self._update_gw_entities()
|
37
40
|
if self.smile(ADAM):
|
38
41
|
self._update_zones()
|
39
42
|
self.gw_entities.update(self._zones)
|
40
43
|
|
41
|
-
self.
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
"notifications": self._notifications,
|
46
|
-
"reboot": True,
|
47
|
-
"smile_name": self.smile_name,
|
48
|
-
}
|
49
|
-
)
|
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
|
50
48
|
if self._is_thermostat:
|
51
|
-
self.
|
52
|
-
|
53
|
-
)
|
49
|
+
self._smile_props["heater_id"] = self._heater_id
|
50
|
+
self._smile_props["cooling_present"] = self._cooling_present
|
54
51
|
|
55
52
|
def _update_zones(self) -> None:
|
56
53
|
"""Helper-function for _all_entity_data() and async_update().
|
@@ -69,7 +66,7 @@ class SmileData(SmileHelper):
|
|
69
66
|
mac_list: list[str] = []
|
70
67
|
for entity_id, entity in self.gw_entities.items():
|
71
68
|
data = self._get_entity_data(entity_id)
|
72
|
-
if entity_id == self.
|
69
|
+
if entity_id == self._gateway_id:
|
73
70
|
mac_list = self._detect_low_batteries()
|
74
71
|
self._add_or_update_notifications(entity_id, entity, data)
|
75
72
|
|
@@ -78,7 +75,13 @@ class SmileData(SmileHelper):
|
|
78
75
|
mac_list
|
79
76
|
and "low_battery" in entity["binary_sensors"]
|
80
77
|
and entity["zigbee_mac_address"] in mac_list
|
81
|
-
and entity["dev_class"]
|
78
|
+
and entity["dev_class"]
|
79
|
+
in (
|
80
|
+
"thermo_sensor",
|
81
|
+
"thermostatic_radiator_valve",
|
82
|
+
"zone_thermometer",
|
83
|
+
"zone_thermostat",
|
84
|
+
)
|
82
85
|
)
|
83
86
|
if is_battery_low:
|
84
87
|
entity["binary_sensors"]["low_battery"] = True
|
@@ -98,7 +101,11 @@ class SmileData(SmileHelper):
|
|
98
101
|
message: str | None = notification.get("message")
|
99
102
|
warning: str | None = notification.get("warning")
|
100
103
|
notify = message or warning
|
101
|
-
if
|
104
|
+
if (
|
105
|
+
notify is not None
|
106
|
+
and all(x in notify for x in matches)
|
107
|
+
and (mac_addresses := mac_pattern.findall(notify))
|
108
|
+
):
|
102
109
|
mac_address = mac_addresses[0] # re.findall() outputs a list
|
103
110
|
|
104
111
|
if mac_address is not None:
|
@@ -113,16 +120,15 @@ class SmileData(SmileHelper):
|
|
113
120
|
) -> None:
|
114
121
|
"""Helper-function adding or updating the Plugwise notifications."""
|
115
122
|
if (
|
116
|
-
entity_id == self.
|
117
|
-
and (
|
118
|
-
self._is_thermostat or self.smile_type == "power"
|
119
|
-
)
|
123
|
+
entity_id == self._gateway_id
|
124
|
+
and (self._is_thermostat or self.smile_type == "power")
|
120
125
|
) or (
|
121
126
|
"binary_sensors" in entity
|
122
127
|
and "plugwise_notification" in entity["binary_sensors"]
|
123
128
|
):
|
124
129
|
data["binary_sensors"]["plugwise_notification"] = bool(self._notifications)
|
125
|
-
self.
|
130
|
+
data["notifications"] = self._notifications
|
131
|
+
self._count += 2
|
126
132
|
|
127
133
|
def _update_for_cooling(self, entity: GwEntityData) -> None:
|
128
134
|
"""Helper-function for adding/updating various cooling-related values."""
|
@@ -152,7 +158,6 @@ class SmileData(SmileHelper):
|
|
152
158
|
sensors["setpoint_high"] = temp_dict["setpoint_high"]
|
153
159
|
self._count += 2 # add 4, remove 2
|
154
160
|
|
155
|
-
|
156
161
|
def _get_location_data(self, loc_id: str) -> GwEntityData:
|
157
162
|
"""Helper-function for _all_entity_data() and async_update().
|
158
163
|
|
@@ -160,13 +165,14 @@ class SmileData(SmileHelper):
|
|
160
165
|
"""
|
161
166
|
zone = self._zones[loc_id]
|
162
167
|
data = self._get_zone_data(loc_id)
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
168
|
+
data["control_state"] = "idle"
|
169
|
+
self._count += 1
|
170
|
+
if (ctrl_state := self._control_state(data, loc_id)) and str(ctrl_state) in (
|
171
|
+
"cooling",
|
172
|
+
"heating",
|
173
|
+
"preheating",
|
174
|
+
):
|
175
|
+
data["control_state"] = str(ctrl_state)
|
170
176
|
|
171
177
|
data["sensors"].pop("setpoint") # remove, only used in _control_state()
|
172
178
|
self._count -= 1
|
@@ -204,6 +210,7 @@ class SmileData(SmileHelper):
|
|
204
210
|
# Thermostat data for Anna (presets, temperatures etc)
|
205
211
|
if self.smile(ANNA) and entity["dev_class"] == "thermostat":
|
206
212
|
self._climate_data(entity_id, entity, data)
|
213
|
+
self._get_anna_control_state(data)
|
207
214
|
|
208
215
|
return data
|
209
216
|
|
@@ -235,7 +242,10 @@ class SmileData(SmileHelper):
|
|
235
242
|
data["binary_sensors"]["heating_state"] = self._heating_valves() != 0
|
236
243
|
# Add cooling_enabled binary_sensor
|
237
244
|
if "binary_sensors" in data:
|
238
|
-
if
|
245
|
+
if (
|
246
|
+
"cooling_enabled" not in data["binary_sensors"]
|
247
|
+
and self._cooling_present
|
248
|
+
):
|
239
249
|
data["binary_sensors"]["cooling_enabled"] = self._cooling_enabled
|
240
250
|
|
241
251
|
# Show the allowed regulation_modes and gateway_modes
|
@@ -248,10 +258,7 @@ class SmileData(SmileHelper):
|
|
248
258
|
self._count += 1
|
249
259
|
|
250
260
|
def _climate_data(
|
251
|
-
self,
|
252
|
-
location_id: str,
|
253
|
-
entity: GwEntityData,
|
254
|
-
data: GwEntityData
|
261
|
+
self, location_id: str, entity: GwEntityData, data: GwEntityData
|
255
262
|
) -> None:
|
256
263
|
"""Helper-function for _get_entity_data().
|
257
264
|
|
@@ -282,7 +289,9 @@ class SmileData(SmileHelper):
|
|
282
289
|
if sel_schedule in (NONE, OFF):
|
283
290
|
data["climate_mode"] = "heat"
|
284
291
|
if self._cooling_present:
|
285
|
-
data["climate_mode"] =
|
292
|
+
data["climate_mode"] = (
|
293
|
+
"cool" if self.check_reg_mode("cooling") else "heat_cool"
|
294
|
+
)
|
286
295
|
|
287
296
|
if self.check_reg_mode("off"):
|
288
297
|
data["climate_mode"] = "off"
|
@@ -294,11 +303,25 @@ class SmileData(SmileHelper):
|
|
294
303
|
|
295
304
|
def check_reg_mode(self, mode: str) -> bool:
|
296
305
|
"""Helper-function for device_data_climate()."""
|
297
|
-
gateway = self.gw_entities[self.
|
306
|
+
gateway = self.gw_entities[self._gateway_id]
|
298
307
|
return (
|
299
308
|
"regulation_modes" in gateway and gateway["select_regulation_mode"] == mode
|
300
309
|
)
|
301
310
|
|
311
|
+
def _get_anna_control_state(self, data: GwEntityData) -> None:
|
312
|
+
"""Set the thermostat control_state based on the opentherm/onoff device state."""
|
313
|
+
data["control_state"] = "idle"
|
314
|
+
self._count += 1
|
315
|
+
for entity in self.gw_entities.values():
|
316
|
+
if entity["dev_class"] != "heater_central":
|
317
|
+
continue
|
318
|
+
|
319
|
+
binary_sensors = entity["binary_sensors"]
|
320
|
+
if binary_sensors["heating_state"]:
|
321
|
+
data["control_state"] = "heating"
|
322
|
+
if binary_sensors.get("cooling_state"):
|
323
|
+
data["control_state"] = "cooling"
|
324
|
+
|
302
325
|
def _get_schedule_states_with_off(
|
303
326
|
self, location: str, schedules: list[str], selected: str, data: GwEntityData
|
304
327
|
) -> None:
|