plugwise 1.7.5__tar.gz → 1.7.7__tar.gz
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-1.7.5 → plugwise-1.7.7}/PKG-INFO +2 -1
- {plugwise-1.7.5 → plugwise-1.7.7}/plugwise/__init__.py +59 -54
- {plugwise-1.7.5 → plugwise-1.7.7}/plugwise/common.py +15 -5
- {plugwise-1.7.5 → plugwise-1.7.7}/plugwise/constants.py +3 -1
- {plugwise-1.7.5 → plugwise-1.7.7}/plugwise/data.py +12 -12
- {plugwise-1.7.5 → plugwise-1.7.7}/plugwise/helper.py +19 -36
- {plugwise-1.7.5 → plugwise-1.7.7}/plugwise/legacy/helper.py +15 -30
- {plugwise-1.7.5 → plugwise-1.7.7}/plugwise/legacy/smile.py +34 -36
- {plugwise-1.7.5 → plugwise-1.7.7}/plugwise/smile.py +77 -61
- {plugwise-1.7.5 → plugwise-1.7.7}/plugwise.egg-info/PKG-INFO +2 -1
- {plugwise-1.7.5 → plugwise-1.7.7}/plugwise.egg-info/requires.txt +1 -0
- {plugwise-1.7.5 → plugwise-1.7.7}/pyproject.toml +2 -1
- {plugwise-1.7.5 → plugwise-1.7.7}/tests/test_adam.py +40 -19
- {plugwise-1.7.5 → plugwise-1.7.7}/tests/test_anna.py +31 -31
- {plugwise-1.7.5 → plugwise-1.7.7}/tests/test_init.py +98 -143
- {plugwise-1.7.5 → plugwise-1.7.7}/tests/test_legacy_anna.py +4 -4
- {plugwise-1.7.5 → plugwise-1.7.7}/tests/test_legacy_p1.py +5 -5
- {plugwise-1.7.5 → plugwise-1.7.7}/tests/test_legacy_stretch.py +7 -7
- {plugwise-1.7.5 → plugwise-1.7.7}/tests/test_p1.py +5 -5
- {plugwise-1.7.5 → plugwise-1.7.7}/LICENSE +0 -0
- {plugwise-1.7.5 → plugwise-1.7.7}/README.md +0 -0
- {plugwise-1.7.5 → plugwise-1.7.7}/plugwise/exceptions.py +0 -0
- {plugwise-1.7.5 → plugwise-1.7.7}/plugwise/legacy/data.py +0 -0
- {plugwise-1.7.5 → plugwise-1.7.7}/plugwise/py.typed +0 -0
- {plugwise-1.7.5 → plugwise-1.7.7}/plugwise/smilecomm.py +0 -0
- {plugwise-1.7.5 → plugwise-1.7.7}/plugwise/util.py +0 -0
- {plugwise-1.7.5 → plugwise-1.7.7}/plugwise.egg-info/SOURCES.txt +0 -0
- {plugwise-1.7.5 → plugwise-1.7.7}/plugwise.egg-info/dependency_links.txt +0 -0
- {plugwise-1.7.5 → plugwise-1.7.7}/plugwise.egg-info/top_level.txt +0 -0
- {plugwise-1.7.5 → plugwise-1.7.7}/setup.cfg +0 -0
- {plugwise-1.7.5 → plugwise-1.7.7}/tests/test_generic.py +0 -0
- {plugwise-1.7.5 → plugwise-1.7.7}/tests/test_legacy_generic.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: plugwise
|
3
|
-
Version: 1.7.
|
3
|
+
Version: 1.7.7
|
4
4
|
Summary: Plugwise Smile (Adam/Anna/P1) and Stretch module for Python 3.
|
5
5
|
Author: Plugwise device owners
|
6
6
|
Maintainer: bouwew, CoMPaTech
|
@@ -16,6 +16,7 @@ Classifier: Topic :: Home Automation
|
|
16
16
|
Requires-Python: >=3.13
|
17
17
|
Description-Content-Type: text/markdown
|
18
18
|
License-File: LICENSE
|
19
|
+
Requires-Dist: aiofiles
|
19
20
|
Requires-Dist: aiohttp
|
20
21
|
Requires-Dist: defusedxml
|
21
22
|
Requires-Dist: munch
|
@@ -5,6 +5,8 @@ Plugwise backend module for Home Assistant Core.
|
|
5
5
|
|
6
6
|
from __future__ import annotations
|
7
7
|
|
8
|
+
from typing import cast
|
9
|
+
|
8
10
|
from plugwise.constants import (
|
9
11
|
DEFAULT_LEGACY_TIMEOUT,
|
10
12
|
DEFAULT_PORT,
|
@@ -15,6 +17,8 @@ from plugwise.constants import (
|
|
15
17
|
MODULES,
|
16
18
|
NONE,
|
17
19
|
SMILES,
|
20
|
+
STATE_OFF,
|
21
|
+
STATE_ON,
|
18
22
|
STATUS,
|
19
23
|
SYSTEM,
|
20
24
|
GwEntityData,
|
@@ -34,6 +38,7 @@ from plugwise.smilecomm import SmileComm
|
|
34
38
|
|
35
39
|
import aiohttp
|
36
40
|
from defusedxml import ElementTree as etree
|
41
|
+
from munch import Munch
|
37
42
|
from packaging.version import Version, parse
|
38
43
|
|
39
44
|
|
@@ -70,16 +75,17 @@ class Smile(SmileComm):
|
|
70
75
|
self._smile_api: SmileAPI | SmileLegacyAPI
|
71
76
|
self._stretch_v2 = False
|
72
77
|
self._target_smile: str = NONE
|
73
|
-
self.
|
74
|
-
self.
|
75
|
-
self.
|
76
|
-
self.
|
77
|
-
self.
|
78
|
-
self.
|
79
|
-
self.
|
80
|
-
self.
|
81
|
-
self.
|
82
|
-
self.
|
78
|
+
self.smile: Munch = Munch()
|
79
|
+
self.smile.hostname = NONE
|
80
|
+
self.smile.hw_version = None
|
81
|
+
self.smile.legacy = False
|
82
|
+
self.smile.mac_address = None
|
83
|
+
self.smile.model = NONE
|
84
|
+
self.smile.model_id = None
|
85
|
+
self.smile.name = NONE
|
86
|
+
self.smile.type = NONE
|
87
|
+
self.smile.version = Version("0.0.0")
|
88
|
+
self.smile.zigbee_mac_address = None
|
83
89
|
|
84
90
|
@property
|
85
91
|
def cooling_present(self) -> bool:
|
@@ -107,7 +113,7 @@ class Smile(SmileComm):
|
|
107
113
|
|
108
114
|
All non-legacy devices support gateway-rebooting.
|
109
115
|
"""
|
110
|
-
return not self.
|
116
|
+
return not self.smile.legacy
|
111
117
|
|
112
118
|
async def connect(self) -> Version:
|
113
119
|
"""Connect to the Plugwise Gateway and determine its name, type, version, and other data."""
|
@@ -156,16 +162,9 @@ class Smile(SmileComm):
|
|
156
162
|
self._opentherm_device,
|
157
163
|
self._request,
|
158
164
|
self._schedule_old_states,
|
159
|
-
self.
|
160
|
-
self.smile_hw_version,
|
161
|
-
self.smile_mac_address,
|
162
|
-
self.smile_model,
|
163
|
-
self.smile_model_id,
|
164
|
-
self.smile_name,
|
165
|
-
self.smile_type,
|
166
|
-
self.smile_version,
|
165
|
+
self.smile,
|
167
166
|
)
|
168
|
-
if not self.
|
167
|
+
if not self.smile.legacy
|
169
168
|
else SmileLegacyAPI(
|
170
169
|
self._is_thermostat,
|
171
170
|
self._loc_data,
|
@@ -174,21 +173,14 @@ class Smile(SmileComm):
|
|
174
173
|
self._request,
|
175
174
|
self._stretch_v2,
|
176
175
|
self._target_smile,
|
177
|
-
self.
|
178
|
-
self.smile_hw_version,
|
179
|
-
self.smile_mac_address,
|
180
|
-
self.smile_model,
|
181
|
-
self.smile_name,
|
182
|
-
self.smile_type,
|
183
|
-
self.smile_version,
|
184
|
-
self.smile_zigbee_mac_address,
|
176
|
+
self.smile,
|
185
177
|
)
|
186
178
|
)
|
187
179
|
|
188
180
|
# Update all endpoints on first connect
|
189
181
|
await self._smile_api.full_xml_update()
|
190
182
|
|
191
|
-
return self.
|
183
|
+
return cast(Version, self.smile.version)
|
192
184
|
|
193
185
|
async def _smile_detect(
|
194
186
|
self, result: etree.Element, dsmrmain: etree.Element
|
@@ -201,15 +193,17 @@ class Smile(SmileComm):
|
|
201
193
|
if (gateway := result.find("./gateway")) is not None:
|
202
194
|
if (v_model := gateway.find("vendor_model")) is not None:
|
203
195
|
model = v_model.text
|
204
|
-
self.
|
205
|
-
self.
|
206
|
-
self.
|
207
|
-
self.
|
208
|
-
self.
|
196
|
+
self.smile.version = parse(gateway.find("firmware_version").text)
|
197
|
+
self.smile.hw_version = gateway.find("hardware_version").text
|
198
|
+
self.smile.hostname = gateway.find("hostname").text
|
199
|
+
self.smile.mac_address = gateway.find("mac_address").text
|
200
|
+
self.smile.model_id = gateway.find("vendor_model").text
|
209
201
|
else:
|
210
202
|
model = await self._smile_detect_legacy(result, dsmrmain, model)
|
211
203
|
|
212
|
-
if model == "Unknown" or self.
|
204
|
+
if model == "Unknown" or self.smile.version == Version(
|
205
|
+
"0.0.0"
|
206
|
+
): # pragma: no cover
|
213
207
|
# Corner case check
|
214
208
|
LOGGER.error(
|
215
209
|
"Unable to find model or version information, please create"
|
@@ -217,7 +211,7 @@ class Smile(SmileComm):
|
|
217
211
|
)
|
218
212
|
raise UnsupportedDeviceError
|
219
213
|
|
220
|
-
version_major = str(self.
|
214
|
+
version_major = str(self.smile.version.major)
|
221
215
|
self._target_smile = f"{model}_v{version_major}"
|
222
216
|
LOGGER.debug("Plugwise identified as %s", self._target_smile)
|
223
217
|
if self._target_smile not in SMILES:
|
@@ -228,7 +222,7 @@ class Smile(SmileComm):
|
|
228
222
|
)
|
229
223
|
raise UnsupportedDeviceError
|
230
224
|
|
231
|
-
if not self.
|
225
|
+
if not self.smile.legacy:
|
232
226
|
self._timeout = DEFAULT_TIMEOUT
|
233
227
|
|
234
228
|
if self._target_smile in ("smile_open_therm_v2", "smile_thermo_v3"):
|
@@ -238,14 +232,14 @@ class Smile(SmileComm):
|
|
238
232
|
) # pragma: no cover
|
239
233
|
raise UnsupportedDeviceError # pragma: no cover
|
240
234
|
|
241
|
-
self.
|
242
|
-
self.
|
243
|
-
self.
|
235
|
+
self.smile.model = "Gateway"
|
236
|
+
self.smile.name = SMILES[self._target_smile].smile_name
|
237
|
+
self.smile.type = SMILES[self._target_smile].smile_type
|
244
238
|
|
245
|
-
if self.
|
239
|
+
if self.smile.type == "stretch":
|
246
240
|
self._stretch_v2 = int(version_major) == 2
|
247
241
|
|
248
|
-
if self.
|
242
|
+
if self.smile.type == "thermostat":
|
249
243
|
self._is_thermostat = True
|
250
244
|
# For Adam, Anna, determine the system capabilities:
|
251
245
|
# Find the connected heating/cooling device (heater_central),
|
@@ -273,13 +267,13 @@ class Smile(SmileComm):
|
|
273
267
|
return_model = model
|
274
268
|
# Stretch: find the MAC of the zigbee master_controller (= Stick)
|
275
269
|
if (network := result.find("./module/protocols/master_controller")) is not None:
|
276
|
-
self.
|
270
|
+
self.smile.zigbee_mac_address = network.find("mac_address").text
|
277
271
|
# Find the active MAC in case there is an orphaned Stick
|
278
272
|
if zb_networks := result.findall("./network"):
|
279
273
|
for zb_network in zb_networks:
|
280
274
|
if zb_network.find("./nodes/network_router") is not None:
|
281
275
|
network = zb_network.find("./master_controller")
|
282
|
-
self.
|
276
|
+
self.smile.zigbee_mac_address = network.find("mac_address").text
|
283
277
|
|
284
278
|
# Legacy Anna or Stretch:
|
285
279
|
if (
|
@@ -287,22 +281,22 @@ class Smile(SmileComm):
|
|
287
281
|
or network is not None
|
288
282
|
):
|
289
283
|
system = await self._request(SYSTEM)
|
290
|
-
self.
|
284
|
+
self.smile.version = parse(system.find("./gateway/firmware").text)
|
291
285
|
return_model = str(system.find("./gateway/product").text)
|
292
|
-
self.
|
286
|
+
self.smile.hostname = system.find("./gateway/hostname").text
|
293
287
|
# If wlan0 contains data it's active, eth0 should be checked last as is preferred
|
294
288
|
for network in ("wlan0", "eth0"):
|
295
289
|
locator = f"./{network}/mac"
|
296
290
|
if (net_locator := system.find(locator)) is not None:
|
297
|
-
self.
|
291
|
+
self.smile.mac_address = net_locator.text
|
298
292
|
|
299
293
|
# P1 legacy:
|
300
294
|
elif dsmrmain is not None:
|
301
295
|
status = await self._request(STATUS)
|
302
|
-
self.
|
296
|
+
self.smile.version = parse(status.find("./system/version").text)
|
303
297
|
return_model = str(status.find("./system/product").text)
|
304
|
-
self.
|
305
|
-
self.
|
298
|
+
self.smile.hostname = status.find("./network/hostname").text
|
299
|
+
self.smile.mac_address = status.find("./network/mac_address").text
|
306
300
|
else: # pragma: no cover
|
307
301
|
# No cornercase, just end of the line
|
308
302
|
LOGGER.error(
|
@@ -311,7 +305,7 @@ class Smile(SmileComm):
|
|
311
305
|
)
|
312
306
|
raise ResponseError
|
313
307
|
|
314
|
-
self.
|
308
|
+
self.smile.legacy = True
|
315
309
|
return return_model
|
316
310
|
|
317
311
|
async def async_update(self) -> dict[str, GwEntityData]:
|
@@ -398,10 +392,21 @@ class Smile(SmileComm):
|
|
398
392
|
|
399
393
|
async def set_switch_state(
|
400
394
|
self, appl_id: str, members: list[str] | None, model: str, state: str
|
401
|
-
) ->
|
402
|
-
"""Set the given State of the relevant Switch.
|
395
|
+
) -> bool:
|
396
|
+
"""Set the given State of the relevant Switch.
|
397
|
+
|
398
|
+
Return the result:
|
399
|
+
- True when switched to state on,
|
400
|
+
- False when switched to state off,
|
401
|
+
- the unchanged state when the switch is for instance locked.
|
402
|
+
"""
|
403
|
+
if state not in (STATE_OFF, STATE_ON):
|
404
|
+
raise PlugwiseError("Invalid state supplied to set_switch_state")
|
405
|
+
|
403
406
|
try:
|
404
|
-
await self._smile_api.set_switch_state(
|
407
|
+
return await self._smile_api.set_switch_state(
|
408
|
+
appl_id, members, model, state
|
409
|
+
)
|
405
410
|
except ConnectionFailedError as exc:
|
406
411
|
raise ConnectionFailedError(
|
407
412
|
f"Failed to set switch state: {str(exc)}"
|
@@ -10,6 +10,7 @@ from typing import cast
|
|
10
10
|
from plugwise.constants import (
|
11
11
|
ANNA,
|
12
12
|
NONE,
|
13
|
+
PRIORITY_DEVICE_CLASSES,
|
13
14
|
SPECIAL_PLUG_TYPES,
|
14
15
|
SWITCH_GROUP_TYPES,
|
15
16
|
ApplianceType,
|
@@ -55,17 +56,16 @@ class SmileCommon:
|
|
55
56
|
self._heater_id: str = NONE
|
56
57
|
self._on_off_device: bool
|
57
58
|
self.gw_entities: dict[str, GwEntityData] = {}
|
58
|
-
self.
|
59
|
-
self.smile_type: str
|
59
|
+
self.smile: Munch
|
60
60
|
|
61
61
|
@property
|
62
62
|
def heater_id(self) -> str:
|
63
63
|
"""Return the heater-id."""
|
64
64
|
return self._heater_id
|
65
65
|
|
66
|
-
def
|
66
|
+
def check_name(self, name: str) -> bool:
|
67
67
|
"""Helper-function checking the smile-name."""
|
68
|
-
return self.
|
68
|
+
return bool(self.smile.name == name)
|
69
69
|
|
70
70
|
def _appl_heater_central_info(
|
71
71
|
self,
|
@@ -153,6 +153,16 @@ class SmileCommon:
|
|
153
153
|
self.gw_entities[appl.entity_id][appl_key] = value
|
154
154
|
self._count += 1
|
155
155
|
|
156
|
+
def _reorder_devices(self) -> None:
|
157
|
+
"""Place the gateway and optional heater_central devices as 1st and 2nd."""
|
158
|
+
reordered = {}
|
159
|
+
for dev_class in PRIORITY_DEVICE_CLASSES:
|
160
|
+
for entity_id, entity in dict(self.gw_entities).items():
|
161
|
+
if entity["dev_class"] == dev_class:
|
162
|
+
reordered[entity_id] = self.gw_entities.pop(entity_id)
|
163
|
+
break
|
164
|
+
self.gw_entities = {**reordered, **self.gw_entities}
|
165
|
+
|
156
166
|
def _entity_switching_group(self, entity: GwEntityData, data: GwEntityData) -> None:
|
157
167
|
"""Helper-function for _get_device_zone_data().
|
158
168
|
|
@@ -173,7 +183,7 @@ class SmileCommon:
|
|
173
183
|
"""
|
174
184
|
switch_groups: dict[str, GwEntityData] = {}
|
175
185
|
# P1 and Anna don't have switchgroups
|
176
|
-
if self.
|
186
|
+
if self.smile.type == "power" or self.check_name(ANNA):
|
177
187
|
return switch_groups
|
178
188
|
|
179
189
|
for group in self._domain_objects.findall("./group"):
|
@@ -23,6 +23,8 @@ POWER_WATT: Final = "W"
|
|
23
23
|
PRESET_AWAY: Final = "away"
|
24
24
|
PRESSURE_BAR: Final = "bar"
|
25
25
|
SIGNAL_STRENGTH_DECIBELS_MILLIWATT: Final = "dBm"
|
26
|
+
STATE_OFF: Final = "off"
|
27
|
+
STATE_ON: Final = "on"
|
26
28
|
TEMP_CELSIUS: Final = "°C"
|
27
29
|
TEMP_KELVIN: Final = "°K"
|
28
30
|
TIME_MILLISECONDS: Final = "ms"
|
@@ -84,7 +86,7 @@ MIN_SETPOINT: Final[float] = 4.0
|
|
84
86
|
MODULE_LOCATOR: Final = "./logs/point_log/*[@id]"
|
85
87
|
NONE: Final = "None"
|
86
88
|
OFF: Final = "off"
|
87
|
-
PRIORITY_DEVICE_CLASSES = ("
|
89
|
+
PRIORITY_DEVICE_CLASSES = ("gateway", "heater_central")
|
88
90
|
|
89
91
|
# XML data paths
|
90
92
|
APPLIANCES: Final = "/core/appliances"
|
@@ -35,7 +35,7 @@ class SmileData(SmileHelper):
|
|
35
35
|
Collect data for each entity and add to self.gw_entities.
|
36
36
|
"""
|
37
37
|
self._update_gw_entities()
|
38
|
-
if self.
|
38
|
+
if self.check_name(ADAM):
|
39
39
|
self._update_zones()
|
40
40
|
self.gw_entities.update(self._zones)
|
41
41
|
|
@@ -86,7 +86,7 @@ class SmileData(SmileHelper):
|
|
86
86
|
mac_pattern = re.compile(r"(?:[0-9A-F]{2}){8}")
|
87
87
|
matches = ["Battery", "below"]
|
88
88
|
if self._notifications:
|
89
|
-
for msg_id, notification in
|
89
|
+
for msg_id, notification in self._notifications.copy().items():
|
90
90
|
mac_address: str | None = None
|
91
91
|
message: str | None = notification.get("message")
|
92
92
|
warning: str | None = notification.get("warning")
|
@@ -111,7 +111,7 @@ class SmileData(SmileHelper):
|
|
111
111
|
"""Helper-function adding or updating the Plugwise notifications."""
|
112
112
|
if (
|
113
113
|
entity_id == self._gateway_id
|
114
|
-
and (self._is_thermostat or self.
|
114
|
+
and (self._is_thermostat or self.smile.type == "power")
|
115
115
|
) or (
|
116
116
|
"binary_sensors" in entity
|
117
117
|
and "plugwise_notification" in entity["binary_sensors"]
|
@@ -124,7 +124,7 @@ class SmileData(SmileHelper):
|
|
124
124
|
"""Helper-function for adding/updating various cooling-related values."""
|
125
125
|
# For Anna and heating + cooling, replace setpoint with setpoint_high/_low
|
126
126
|
if (
|
127
|
-
self.
|
127
|
+
self.check_name(ANNA)
|
128
128
|
and self._cooling_present
|
129
129
|
and entity["dev_class"] == "thermostat"
|
130
130
|
):
|
@@ -194,11 +194,11 @@ class SmileData(SmileHelper):
|
|
194
194
|
# Switching groups data
|
195
195
|
self._entity_switching_group(entity, data)
|
196
196
|
# Adam data
|
197
|
-
if self.
|
197
|
+
if self.check_name(ADAM):
|
198
198
|
self._get_adam_data(entity, data)
|
199
199
|
|
200
200
|
# Thermostat data for Anna (presets, temperatures etc)
|
201
|
-
if self.
|
201
|
+
if self.check_name(ANNA) and entity["dev_class"] == "thermostat":
|
202
202
|
self._climate_data(entity_id, entity, data)
|
203
203
|
self._get_anna_control_state(data)
|
204
204
|
|
@@ -232,12 +232,12 @@ class SmileData(SmileHelper):
|
|
232
232
|
if self._on_off_device and isinstance(self._heating_valves(), int):
|
233
233
|
data["binary_sensors"]["heating_state"] = self._heating_valves() != 0
|
234
234
|
# Add cooling_enabled binary_sensor
|
235
|
-
if
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
235
|
+
if (
|
236
|
+
"binary_sensors" in data
|
237
|
+
and "cooling_enabled" not in data["binary_sensors"]
|
238
|
+
and self._cooling_present
|
239
|
+
):
|
240
|
+
data["binary_sensors"]["cooling_enabled"] = self._cooling_enabled
|
241
241
|
|
242
242
|
# Show the allowed regulation_modes and gateway_modes
|
243
243
|
if entity["dev_class"] == "gateway":
|
@@ -28,7 +28,6 @@ from plugwise.constants import (
|
|
28
28
|
NONE,
|
29
29
|
OFF,
|
30
30
|
P1_MEASUREMENTS,
|
31
|
-
PRIORITY_DEVICE_CLASSES,
|
32
31
|
TEMP_CELSIUS,
|
33
32
|
THERMOSTAT_CLASSES,
|
34
33
|
TOGGLES,
|
@@ -85,11 +84,7 @@ class SmileHelper(SmileCommon):
|
|
85
84
|
self._gateway_id: str = NONE
|
86
85
|
self._zones: dict[str, GwEntityData]
|
87
86
|
self.gw_entities: dict[str, GwEntityData]
|
88
|
-
self.
|
89
|
-
self.smile_mac_address: str | None
|
90
|
-
self.smile_model: str
|
91
|
-
self.smile_model_id: str | None
|
92
|
-
self.smile_version: version.Version
|
87
|
+
self.smile: Munch = Munch()
|
93
88
|
|
94
89
|
@property
|
95
90
|
def gateway_id(self) -> str:
|
@@ -160,11 +155,11 @@ class SmileHelper(SmileCommon):
|
|
160
155
|
|
161
156
|
self._create_gw_entities(appl)
|
162
157
|
|
163
|
-
if self.
|
158
|
+
if self.smile.type == "power":
|
164
159
|
self._get_p1_smartmeter_info()
|
165
160
|
|
166
161
|
# Sort the gw_entities
|
167
|
-
self.
|
162
|
+
self._reorder_devices()
|
168
163
|
|
169
164
|
def _get_p1_smartmeter_info(self) -> None:
|
170
165
|
"""For P1 collect the connected SmartMeter info from the Home/building location.
|
@@ -197,18 +192,6 @@ class SmileHelper(SmileCommon):
|
|
197
192
|
|
198
193
|
self._create_gw_entities(appl)
|
199
194
|
|
200
|
-
def _sort_gw_entities(self) -> None:
|
201
|
-
"""Place the gateway and optional heater_central entities as 1st and 2nd."""
|
202
|
-
for dev_class in PRIORITY_DEVICE_CLASSES:
|
203
|
-
for entity_id, entity in dict(self.gw_entities).items():
|
204
|
-
if entity["dev_class"] == dev_class:
|
205
|
-
priority_entity = entity
|
206
|
-
self.gw_entities.pop(entity_id)
|
207
|
-
other_entities = self.gw_entities
|
208
|
-
priority_entities = {entity_id: priority_entity}
|
209
|
-
self.gw_entities = {**priority_entities, **other_entities}
|
210
|
-
break
|
211
|
-
|
212
195
|
def _all_locations(self) -> None:
|
213
196
|
"""Collect all locations."""
|
214
197
|
loc = Munch()
|
@@ -268,16 +251,16 @@ class SmileHelper(SmileCommon):
|
|
268
251
|
def _appl_gateway_info(self, appl: Munch, appliance: etree.Element) -> Munch:
|
269
252
|
"""Helper-function for _appliance_info_finder()."""
|
270
253
|
self._gateway_id = appliance.attrib["id"]
|
271
|
-
appl.firmware = str(self.
|
272
|
-
appl.hardware = self.
|
273
|
-
appl.mac = self.
|
274
|
-
appl.model = self.
|
275
|
-
appl.model_id = self.
|
276
|
-
appl.name = self.
|
254
|
+
appl.firmware = str(self.smile.version)
|
255
|
+
appl.hardware = self.smile.hw_version
|
256
|
+
appl.mac = self.smile.mac_address
|
257
|
+
appl.model = self.smile.model
|
258
|
+
appl.model_id = self.smile.model_id
|
259
|
+
appl.name = self.smile.name
|
277
260
|
appl.vendor_name = "Plugwise"
|
278
261
|
|
279
262
|
# Adam: collect the ZigBee MAC address of the Smile
|
280
|
-
if self.
|
263
|
+
if self.check_name(ADAM):
|
281
264
|
if (
|
282
265
|
found := self._domain_objects.find(".//protocols/zig_bee_coordinator")
|
283
266
|
) is not None:
|
@@ -346,7 +329,7 @@ class SmileHelper(SmileCommon):
|
|
346
329
|
# Get P1 smartmeter data from LOCATIONS
|
347
330
|
entity = self.gw_entities[entity_id]
|
348
331
|
# !! DON'T CHANGE below two if-lines, will break stuff !!
|
349
|
-
if self.
|
332
|
+
if self.smile.type == "power":
|
350
333
|
if entity["dev_class"] == "smartmeter":
|
351
334
|
data.update(self._power_data_from_location())
|
352
335
|
|
@@ -383,7 +366,7 @@ class SmileHelper(SmileCommon):
|
|
383
366
|
data.pop("c_heating_state")
|
384
367
|
self._count -= 1
|
385
368
|
|
386
|
-
if self._is_thermostat and self.
|
369
|
+
if self._is_thermostat and self.check_name(ANNA):
|
387
370
|
self._update_anna_cooling(entity_id, data)
|
388
371
|
|
389
372
|
self._cleanup_data(data)
|
@@ -484,7 +467,7 @@ class SmileHelper(SmileCommon):
|
|
484
467
|
item == "thermostat"
|
485
468
|
and (
|
486
469
|
entity["dev_class"] != "climate"
|
487
|
-
if self.
|
470
|
+
if self.check_name(ADAM)
|
488
471
|
else entity["dev_class"] != "thermostat"
|
489
472
|
)
|
490
473
|
):
|
@@ -539,7 +522,7 @@ class SmileHelper(SmileCommon):
|
|
539
522
|
|
540
523
|
Collect the requested gateway mode.
|
541
524
|
"""
|
542
|
-
if not (self.
|
525
|
+
if not (self.check_name(ADAM) and entity_id == self._gateway_id):
|
543
526
|
return None
|
544
527
|
|
545
528
|
if (search := search_actuator_functionalities(appliance, key)) is not None:
|
@@ -605,10 +588,10 @@ class SmileHelper(SmileCommon):
|
|
605
588
|
|
606
589
|
Solution for Core issue #81839.
|
607
590
|
"""
|
608
|
-
if self.
|
591
|
+
if self.check_name(ANNA):
|
609
592
|
data["binary_sensors"]["heating_state"] = data["c_heating_state"]
|
610
593
|
|
611
|
-
if self.
|
594
|
+
if self.check_name(ADAM):
|
612
595
|
# First count when not present, then create and init to False.
|
613
596
|
# When present init to False
|
614
597
|
if "heating_state" not in data["binary_sensors"]:
|
@@ -723,7 +706,7 @@ class SmileHelper(SmileCommon):
|
|
723
706
|
for entity_id, entity in self.gw_entities.items():
|
724
707
|
self._rank_thermostat(thermo_matching, loc_id, entity_id, entity)
|
725
708
|
|
726
|
-
for loc_id, loc_data in
|
709
|
+
for loc_id, loc_data in self._thermo_locs.items():
|
727
710
|
if loc_data["primary_prio"] != 0:
|
728
711
|
self._zones[loc_id] = {
|
729
712
|
"dev_class": "climate",
|
@@ -799,8 +782,8 @@ class SmileHelper(SmileCommon):
|
|
799
782
|
|
800
783
|
# Handle missing control_state in regulation_mode off for firmware >= 3.2.0 (issue #776)
|
801
784
|
# In newer firmware versions, default to "off" when control_state is not present
|
802
|
-
if self.
|
803
|
-
if self.
|
785
|
+
if self.smile.version != version.Version("0.0.0"):
|
786
|
+
if self.smile.version >= version.parse("3.2.0"):
|
804
787
|
return "off"
|
805
788
|
|
806
789
|
# Older Adam firmware does not have the control_state xml-key
|
@@ -23,7 +23,6 @@ from plugwise.constants import (
|
|
23
23
|
NONE,
|
24
24
|
OFF,
|
25
25
|
P1_LEGACY_MEASUREMENTS,
|
26
|
-
PRIORITY_DEVICE_CLASSES,
|
27
26
|
TEMP_CELSIUS,
|
28
27
|
THERMOSTAT_CLASSES,
|
29
28
|
UOM,
|
@@ -47,7 +46,6 @@ from plugwise.util import (
|
|
47
46
|
# This way of importing aiohttp is because of patch/mocking in testing (aiohttp timeouts)
|
48
47
|
from defusedxml import ElementTree as etree
|
49
48
|
from munch import Munch
|
50
|
-
from packaging.version import Version
|
51
49
|
|
52
50
|
|
53
51
|
def etree_to_dict(element: etree.Element) -> dict[str, str]:
|
@@ -73,10 +71,7 @@ class SmileLegacyHelper(SmileCommon):
|
|
73
71
|
self._modules: etree.Element
|
74
72
|
self._stretch_v2: bool
|
75
73
|
self.gw_entities: dict[str, GwEntityData] = {}
|
76
|
-
self.
|
77
|
-
self.smile_model: str
|
78
|
-
self.smile_version: Version
|
79
|
-
self.smile_zigbee_mac_address: str | None
|
74
|
+
self.smile: Munch = Munch()
|
80
75
|
|
81
76
|
@property
|
82
77
|
def gateway_id(self) -> str:
|
@@ -95,7 +90,7 @@ class SmileLegacyHelper(SmileCommon):
|
|
95
90
|
|
96
91
|
self._create_legacy_gateway()
|
97
92
|
# For legacy P1 collect the connected SmartMeter info
|
98
|
-
if self.
|
93
|
+
if self.smile.type == "power":
|
99
94
|
appl = Munch()
|
100
95
|
self._p1_smartmeter_info_finder(appl)
|
101
96
|
# Legacy P1 has no more devices
|
@@ -140,17 +135,7 @@ class SmileLegacyHelper(SmileCommon):
|
|
140
135
|
continue # pragma: no cover
|
141
136
|
|
142
137
|
self._create_gw_entities(appl)
|
143
|
-
|
144
|
-
# Place the gateway and optional heater_central devices as 1st and 2nd
|
145
|
-
for dev_class in PRIORITY_DEVICE_CLASSES:
|
146
|
-
for entity_id, entity in dict(self.gw_entities).items():
|
147
|
-
if entity["dev_class"] == dev_class:
|
148
|
-
tmp_entity = entity
|
149
|
-
self.gw_entities.pop(entity_id)
|
150
|
-
cleared_dict = self.gw_entities
|
151
|
-
add_to_front = {entity_id: tmp_entity}
|
152
|
-
self.gw_entities = {**add_to_front, **cleared_dict}
|
153
|
-
break
|
138
|
+
self._reorder_devices()
|
154
139
|
|
155
140
|
def _all_locations(self) -> None:
|
156
141
|
"""Collect all locations."""
|
@@ -167,13 +152,13 @@ class SmileLegacyHelper(SmileCommon):
|
|
167
152
|
loc.loc_id = location.attrib["id"]
|
168
153
|
# Filter the valid single location for P1 legacy: services not empty
|
169
154
|
locator = "./services"
|
170
|
-
if self.
|
155
|
+
if self.smile.type == "power" and len(location.find(locator)) == 0:
|
171
156
|
continue
|
172
157
|
|
173
158
|
if loc.name == "Home":
|
174
159
|
self._home_loc_id = loc.loc_id
|
175
160
|
# Replace location-name for P1 legacy, can contain privacy-related info
|
176
|
-
if self.
|
161
|
+
if self.smile.type == "power":
|
177
162
|
loc.name = "Home"
|
178
163
|
self._home_loc_id = loc.loc_id
|
179
164
|
|
@@ -185,18 +170,18 @@ class SmileLegacyHelper(SmileCommon):
|
|
185
170
|
Use the home_location or FAKE_APPL as entity id.
|
186
171
|
"""
|
187
172
|
self._gateway_id = self._home_loc_id
|
188
|
-
if self.
|
173
|
+
if self.smile.type == "power":
|
189
174
|
self._gateway_id = FAKE_APPL
|
190
175
|
|
191
176
|
self.gw_entities[self._gateway_id] = {"dev_class": "gateway"}
|
192
177
|
self._count += 1
|
193
178
|
for key, value in {
|
194
|
-
"firmware": str(self.
|
179
|
+
"firmware": str(self.smile.version),
|
195
180
|
"location": self._home_loc_id,
|
196
|
-
"mac_address": self.
|
197
|
-
"model": self.
|
198
|
-
"name": self.
|
199
|
-
"zigbee_mac_address": self.
|
181
|
+
"mac_address": self.smile.mac_address,
|
182
|
+
"model": self.smile.model,
|
183
|
+
"name": self.smile.name,
|
184
|
+
"zigbee_mac_address": self.smile.zigbee_mac_address,
|
200
185
|
"vendor": "Plugwise",
|
201
186
|
}.items():
|
202
187
|
if value is not None:
|
@@ -224,14 +209,14 @@ class SmileLegacyHelper(SmileCommon):
|
|
224
209
|
|
225
210
|
Collect energy entity info (Smartmeter, Circle, Stealth, etc.): firmware, model and vendor name.
|
226
211
|
"""
|
227
|
-
if self.
|
212
|
+
if self.smile.type in ("power", "stretch"):
|
228
213
|
locator = "./services/electricity_point_meter"
|
229
214
|
module_data = self._get_module_data(
|
230
215
|
appliance, locator, self._modules, legacy=True
|
231
216
|
)
|
232
217
|
appl.zigbee_mac = module_data["zigbee_mac_address"]
|
233
218
|
# Filter appliance without zigbee_mac, it's an orphaned device
|
234
|
-
if appl.zigbee_mac is None and self.
|
219
|
+
if appl.zigbee_mac is None and self.smile.type != "power":
|
235
220
|
return None
|
236
221
|
|
237
222
|
appl.hardware = module_data["hardware_version"]
|
@@ -253,7 +238,7 @@ class SmileLegacyHelper(SmileCommon):
|
|
253
238
|
appl.entity_id = loc_id
|
254
239
|
appl.location = loc_id
|
255
240
|
appl.mac = None
|
256
|
-
appl.model = self.
|
241
|
+
appl.model = self.smile.model
|
257
242
|
appl.model_id = None
|
258
243
|
appl.name = "P1"
|
259
244
|
appl.pwclass = "smartmeter"
|
@@ -272,7 +257,7 @@ class SmileLegacyHelper(SmileCommon):
|
|
272
257
|
# Get P1 smartmeter data from MODULES
|
273
258
|
entity = self.gw_entities[entity_id]
|
274
259
|
# !! DON'T CHANGE below two if-lines, will break stuff !!
|
275
|
-
if self.
|
260
|
+
if self.smile.type == "power":
|
276
261
|
if entity["dev_class"] == "smartmeter":
|
277
262
|
data.update(self._power_data_from_modules())
|
278
263
|
|