plugwise 1.7.5__py3-none-any.whl → 1.7.7__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 +59 -54
- plugwise/common.py +15 -5
- plugwise/constants.py +3 -1
- plugwise/data.py +12 -12
- plugwise/helper.py +19 -36
- plugwise/legacy/helper.py +15 -30
- plugwise/legacy/smile.py +34 -36
- plugwise/smile.py +77 -61
- {plugwise-1.7.5.dist-info → plugwise-1.7.7.dist-info}/METADATA +2 -1
- plugwise-1.7.7.dist-info/RECORD +18 -0
- plugwise-1.7.5.dist-info/RECORD +0 -18
- {plugwise-1.7.5.dist-info → plugwise-1.7.7.dist-info}/WHEEL +0 -0
- {plugwise-1.7.5.dist-info → plugwise-1.7.7.dist-info}/licenses/LICENSE +0 -0
- {plugwise-1.7.5.dist-info → plugwise-1.7.7.dist-info}/top_level.txt +0 -0
plugwise/__init__.py
CHANGED
@@ -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)}"
|
plugwise/common.py
CHANGED
@@ -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"):
|
plugwise/constants.py
CHANGED
@@ -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"
|
plugwise/data.py
CHANGED
@@ -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":
|
plugwise/helper.py
CHANGED
@@ -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
|
plugwise/legacy/helper.py
CHANGED
@@ -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
|
|
plugwise/legacy/smile.py
CHANGED
@@ -18,6 +18,8 @@ from plugwise.constants import (
|
|
18
18
|
OFF,
|
19
19
|
REQUIRE_APPLIANCES,
|
20
20
|
RULES,
|
21
|
+
STATE_OFF,
|
22
|
+
STATE_ON,
|
21
23
|
GwEntityData,
|
22
24
|
ThermoLoc,
|
23
25
|
)
|
@@ -25,7 +27,6 @@ from plugwise.exceptions import ConnectionFailedError, DataMissingError, Plugwis
|
|
25
27
|
from plugwise.legacy.data import SmileLegacyData
|
26
28
|
|
27
29
|
from munch import Munch
|
28
|
-
from packaging.version import Version
|
29
30
|
|
30
31
|
|
31
32
|
class SmileLegacyAPI(SmileLegacyData):
|
@@ -42,14 +43,7 @@ class SmileLegacyAPI(SmileLegacyData):
|
|
42
43
|
_request: Callable[..., Awaitable[Any]],
|
43
44
|
_stretch_v2: bool,
|
44
45
|
_target_smile: str,
|
45
|
-
|
46
|
-
smile_hw_version: str | None,
|
47
|
-
smile_mac_address: str | None,
|
48
|
-
smile_model: str,
|
49
|
-
smile_name: str,
|
50
|
-
smile_type: str,
|
51
|
-
smile_version: Version,
|
52
|
-
smile_zigbee_mac_address: str | None,
|
46
|
+
smile: Munch,
|
53
47
|
) -> None:
|
54
48
|
"""Set the constructor for this class."""
|
55
49
|
super().__init__()
|
@@ -61,14 +55,7 @@ class SmileLegacyAPI(SmileLegacyData):
|
|
61
55
|
self._request = _request
|
62
56
|
self._stretch_v2 = _stretch_v2
|
63
57
|
self._target_smile = _target_smile
|
64
|
-
self.
|
65
|
-
self.smile_hw_version = smile_hw_version
|
66
|
-
self.smile_mac_address = smile_mac_address
|
67
|
-
self.smile_model = smile_model
|
68
|
-
self.smile_name = smile_name
|
69
|
-
self.smile_type = smile_type
|
70
|
-
self.smile_version = smile_version
|
71
|
-
self.smile_zigbee_mac_address = smile_zigbee_mac_address
|
58
|
+
self.smile = smile
|
72
59
|
|
73
60
|
self._first_update = True
|
74
61
|
self._previous_day_number: str = "0"
|
@@ -84,7 +71,7 @@ class SmileLegacyAPI(SmileLegacyData):
|
|
84
71
|
self._locations = await self._request(LOCATIONS)
|
85
72
|
self._modules = await self._request(MODULES)
|
86
73
|
# P1 legacy has no appliances
|
87
|
-
if self.
|
74
|
+
if self.smile.type != "power":
|
88
75
|
self._appliances = await self._request(APPLIANCES)
|
89
76
|
|
90
77
|
def get_all_gateway_entities(self) -> None:
|
@@ -195,7 +182,7 @@ class SmileLegacyAPI(SmileLegacyData):
|
|
195
182
|
Determined from - DOMAIN_OBJECTS.
|
196
183
|
Used in HA Core to set the hvac_mode: in practice switch between schedule on - off.
|
197
184
|
"""
|
198
|
-
if state not in (
|
185
|
+
if state not in (STATE_OFF, STATE_ON):
|
199
186
|
raise PlugwiseError("Plugwise: invalid schedule state.")
|
200
187
|
|
201
188
|
# Handle no schedule-name / Off-schedule provided
|
@@ -214,7 +201,7 @@ class SmileLegacyAPI(SmileLegacyData):
|
|
214
201
|
) # pragma: no cover
|
215
202
|
|
216
203
|
new_state = "false"
|
217
|
-
if state ==
|
204
|
+
if state == STATE_ON:
|
218
205
|
new_state = "true"
|
219
206
|
|
220
207
|
locator = f'.//*[@id="{schedule_rule_id}"]/template'
|
@@ -234,13 +221,16 @@ class SmileLegacyAPI(SmileLegacyData):
|
|
234
221
|
|
235
222
|
async def set_switch_state(
|
236
223
|
self, appl_id: str, members: list[str] | None, model: str, state: str
|
237
|
-
) ->
|
224
|
+
) -> bool:
|
238
225
|
"""Set the given state of the relevant switch.
|
239
226
|
|
240
227
|
For individual switches, sets the state directly.
|
241
228
|
For group switches, sets the state for each member in the group separately.
|
242
229
|
For switch-locks, sets the lock state using a different data format.
|
230
|
+
Return the requested state when succesful, the current state otherwise.
|
243
231
|
"""
|
232
|
+
current_state = self.gw_entities[appl_id]["switches"]["relay"]
|
233
|
+
requested_state = state == STATE_ON
|
244
234
|
switch = Munch()
|
245
235
|
switch.actuator = "actuator_functionalities"
|
246
236
|
switch.func_type = "relay_functionality"
|
@@ -250,7 +240,7 @@ class SmileLegacyAPI(SmileLegacyData):
|
|
250
240
|
|
251
241
|
# Handle switch-lock
|
252
242
|
if model == "lock":
|
253
|
-
state = "
|
243
|
+
state = "true" if state == STATE_ON else "false"
|
254
244
|
appliance = self._appliances.find(f'appliance[@id="{appl_id}"]')
|
255
245
|
appl_name = appliance.find("name").text
|
256
246
|
appl_type = appliance.find("type").text
|
@@ -269,37 +259,45 @@ class SmileLegacyAPI(SmileLegacyData):
|
|
269
259
|
"</appliances>"
|
270
260
|
)
|
271
261
|
await self.call_request(APPLIANCES, method="post", data=data)
|
272
|
-
return
|
262
|
+
return requested_state
|
273
263
|
|
274
264
|
# Handle group of switches
|
275
265
|
data = f"<{switch.func_type}><state>{state}</state></{switch.func_type}>"
|
276
266
|
if members is not None:
|
277
267
|
return await self._set_groupswitch_member_state(
|
278
|
-
data, members, state, switch
|
268
|
+
appl_id, data, members, state, switch
|
279
269
|
)
|
280
270
|
|
281
271
|
# Handle individual relay switches
|
282
272
|
uri = f"{APPLIANCES};id={appl_id}/relay"
|
283
|
-
if model == "relay":
|
284
|
-
locator = (
|
285
|
-
f'appliance[@id="{appl_id}"]/{switch.actuator}/{switch.func_type}/lock'
|
286
|
-
)
|
273
|
+
if model == "relay" and self.gw_entities[appl_id]["switches"]["lock"]:
|
287
274
|
# Don't bother switching a relay when the corresponding lock-state is true
|
288
|
-
|
289
|
-
raise PlugwiseError("Plugwise: the locked Relay was not switched.")
|
275
|
+
return current_state
|
290
276
|
|
291
277
|
await self.call_request(uri, method="put", data=data)
|
278
|
+
return requested_state
|
292
279
|
|
293
280
|
async def _set_groupswitch_member_state(
|
294
|
-
self, data: str, members: list[str], state: str, switch: Munch
|
295
|
-
) ->
|
281
|
+
self, appl_id: str, data: str, members: list[str], state: str, switch: Munch
|
282
|
+
) -> bool:
|
296
283
|
"""Helper-function for set_switch_state().
|
297
284
|
|
298
|
-
Set the
|
285
|
+
Set the requested state of the relevant switch within a group of switches.
|
286
|
+
Return the current group-state when none of the switches has changed its state, the requested state otherwise.
|
299
287
|
"""
|
288
|
+
current_state = self.gw_entities[appl_id]["switches"]["relay"]
|
289
|
+
requested_state = state == STATE_ON
|
290
|
+
switched = 0
|
300
291
|
for member in members:
|
301
|
-
|
302
|
-
|
292
|
+
if not self.gw_entities[member]["switches"]["lock"]:
|
293
|
+
uri = f"{APPLIANCES};id={member}/relay"
|
294
|
+
await self.call_request(uri, method="put", data=data)
|
295
|
+
switched += 1
|
296
|
+
|
297
|
+
if switched > 0:
|
298
|
+
return requested_state
|
299
|
+
|
300
|
+
return current_state # pragma: no cover
|
303
301
|
|
304
302
|
async def set_temperature(self, _: str, items: dict[str, float]) -> None:
|
305
303
|
"""Set the given Temperature on the relevant Thermostat."""
|
@@ -310,7 +308,7 @@ class SmileLegacyAPI(SmileLegacyData):
|
|
310
308
|
if setpoint is None:
|
311
309
|
raise PlugwiseError(
|
312
310
|
"Plugwise: failed setting temperature: no valid input provided"
|
313
|
-
) # pragma: no cover
|
311
|
+
) # pragma: no cover
|
314
312
|
|
315
313
|
temperature = str(setpoint)
|
316
314
|
data = (
|
plugwise/smile.py
CHANGED
@@ -7,7 +7,7 @@ from __future__ import annotations
|
|
7
7
|
|
8
8
|
from collections.abc import Awaitable, Callable
|
9
9
|
import datetime as dt
|
10
|
-
from typing import Any
|
10
|
+
from typing import Any, cast
|
11
11
|
|
12
12
|
from plugwise.constants import (
|
13
13
|
ADAM,
|
@@ -22,7 +22,10 @@ from plugwise.constants import (
|
|
22
22
|
NOTIFICATIONS,
|
23
23
|
OFF,
|
24
24
|
RULES,
|
25
|
+
STATE_OFF,
|
26
|
+
STATE_ON,
|
25
27
|
GwEntityData,
|
28
|
+
SwitchType,
|
26
29
|
ThermoLoc,
|
27
30
|
)
|
28
31
|
from plugwise.data import SmileData
|
@@ -32,7 +35,27 @@ from defusedxml import ElementTree as etree
|
|
32
35
|
|
33
36
|
# Dict as class
|
34
37
|
from munch import Munch
|
35
|
-
|
38
|
+
|
39
|
+
|
40
|
+
def model_to_switch_items(model: str, state: str, switch: Munch) -> tuple[str, Munch]:
|
41
|
+
"""Translate state and switch attributes based on model name.
|
42
|
+
|
43
|
+
Helper function for set_switch_state().
|
44
|
+
"""
|
45
|
+
match model:
|
46
|
+
case "dhw_cm_switch":
|
47
|
+
switch.device = "toggle"
|
48
|
+
switch.func_type = "toggle_functionality"
|
49
|
+
switch.act_type = "domestic_hot_water_comfort_mode"
|
50
|
+
case "cooling_ena_switch":
|
51
|
+
switch.device = "toggle"
|
52
|
+
switch.func_type = "toggle_functionality"
|
53
|
+
switch.act_type = "cooling_enabled"
|
54
|
+
case "lock":
|
55
|
+
switch.func = "lock"
|
56
|
+
state = "true" if state == STATE_ON else "false"
|
57
|
+
|
58
|
+
return state, switch
|
36
59
|
|
37
60
|
|
38
61
|
class SmileAPI(SmileData):
|
@@ -51,14 +74,7 @@ class SmileAPI(SmileData):
|
|
51
74
|
_opentherm_device: bool,
|
52
75
|
_request: Callable[..., Awaitable[Any]],
|
53
76
|
_schedule_old_states: dict[str, dict[str, str]],
|
54
|
-
|
55
|
-
smile_hw_version: str | None,
|
56
|
-
smile_mac_address: str | None,
|
57
|
-
smile_model: str,
|
58
|
-
smile_model_id: str | None,
|
59
|
-
smile_name: str,
|
60
|
-
smile_type: str,
|
61
|
-
smile_version: Version,
|
77
|
+
smile: Munch,
|
62
78
|
) -> None:
|
63
79
|
"""Set the constructor for this class."""
|
64
80
|
super().__init__()
|
@@ -71,14 +87,7 @@ class SmileAPI(SmileData):
|
|
71
87
|
self._opentherm_device = _opentherm_device
|
72
88
|
self._request = _request
|
73
89
|
self._schedule_old_states = _schedule_old_states
|
74
|
-
self.
|
75
|
-
self.smile_hw_version = smile_hw_version
|
76
|
-
self.smile_mac_address = smile_mac_address
|
77
|
-
self.smile_model = smile_model
|
78
|
-
self.smile_model_id = smile_model_id
|
79
|
-
self.smile_name = smile_name
|
80
|
-
self.smile_type = smile_type
|
81
|
-
self.smile_version = smile_version
|
90
|
+
self.smile = smile
|
82
91
|
self.therms_with_offset_func: list[str] = []
|
83
92
|
|
84
93
|
@property
|
@@ -104,7 +113,7 @@ class SmileAPI(SmileData):
|
|
104
113
|
self.therms_with_offset_func = (
|
105
114
|
self._get_appliances_with_offset_functionality()
|
106
115
|
)
|
107
|
-
if self.
|
116
|
+
if self.check_name(ADAM):
|
108
117
|
self._scan_thermostats()
|
109
118
|
|
110
119
|
if group_data := self._get_group_switches():
|
@@ -309,12 +318,12 @@ class SmileAPI(SmileData):
|
|
309
318
|
Used in HA Core to set the hvac_mode: in practice switch between schedule on - off.
|
310
319
|
"""
|
311
320
|
# Input checking
|
312
|
-
if new_state not in (
|
321
|
+
if new_state not in (STATE_OFF, STATE_ON):
|
313
322
|
raise PlugwiseError("Plugwise: invalid schedule state.")
|
314
323
|
|
315
324
|
# Translate selection of Off-schedule-option to disabling the active schedule
|
316
325
|
if name == OFF:
|
317
|
-
new_state =
|
326
|
+
new_state = STATE_OFF
|
318
327
|
|
319
328
|
# Handle no schedule-name / Off-schedule provided
|
320
329
|
if name is None or name == OFF:
|
@@ -337,7 +346,7 @@ class SmileAPI(SmileData):
|
|
337
346
|
template = (
|
338
347
|
'<template tag="zone_preset_based_on_time_and_presence_with_override" />'
|
339
348
|
)
|
340
|
-
if self.
|
349
|
+
if self.check_name(ANNA):
|
341
350
|
locator = f'.//*[@id="{schedule_rule_id}"]/template'
|
342
351
|
template_id = self._domain_objects.find(locator).attrib["id"]
|
343
352
|
template = f'<template id="{template_id}" />'
|
@@ -367,39 +376,43 @@ class SmileAPI(SmileData):
|
|
367
376
|
subject = f'<context><zone><location id="{loc_id}" /></zone></context>'
|
368
377
|
subject = etree.fromstring(subject)
|
369
378
|
|
370
|
-
if state ==
|
379
|
+
if state == STATE_OFF:
|
371
380
|
self._last_active[loc_id] = name
|
372
381
|
contexts.remove(subject)
|
373
|
-
if state ==
|
382
|
+
if state == STATE_ON:
|
374
383
|
contexts.append(subject)
|
375
384
|
|
376
385
|
return str(etree.tostring(contexts, encoding="unicode").rstrip())
|
377
386
|
|
378
387
|
async def set_switch_state(
|
379
388
|
self, appl_id: str, members: list[str] | None, model: str, state: str
|
380
|
-
) ->
|
381
|
-
"""Set the given
|
389
|
+
) -> bool:
|
390
|
+
"""Set the given state of the relevant Switch.
|
391
|
+
|
392
|
+
For individual switches, sets the state directly.
|
393
|
+
For group switches, sets the state for each member in the group separately.
|
394
|
+
For switch-locks, sets the lock state using a different data format.
|
395
|
+
Return the requested state when succesful, the current state otherwise.
|
396
|
+
"""
|
397
|
+
model_type = cast(SwitchType, model)
|
398
|
+
current_state = self.gw_entities[appl_id]["switches"][model_type]
|
399
|
+
requested_state = state == STATE_ON
|
382
400
|
switch = Munch()
|
383
401
|
switch.actuator = "actuator_functionalities"
|
384
402
|
switch.device = "relay"
|
385
403
|
switch.func_type = "relay_functionality"
|
386
404
|
switch.func = "state"
|
387
|
-
|
388
|
-
|
389
|
-
switch.func_type
|
390
|
-
switch.
|
391
|
-
|
392
|
-
|
393
|
-
switch.device = "toggle"
|
394
|
-
switch.func_type = "toggle_functionality"
|
395
|
-
switch.act_type = "cooling_enabled"
|
396
|
-
|
397
|
-
if model == "lock":
|
398
|
-
switch.func = "lock"
|
399
|
-
state = "false" if state == "off" else "true"
|
405
|
+
state, switch = model_to_switch_items(model, state, switch)
|
406
|
+
data = (
|
407
|
+
f"<{switch.func_type}>"
|
408
|
+
f"<{switch.func}>{state}</{switch.func}>"
|
409
|
+
f"</{switch.func_type}>"
|
410
|
+
)
|
400
411
|
|
401
412
|
if members is not None:
|
402
|
-
return await self._set_groupswitch_member_state(
|
413
|
+
return await self._set_groupswitch_member_state(
|
414
|
+
appl_id, data, members, state, switch
|
415
|
+
)
|
403
416
|
|
404
417
|
locator = f'appliance[@id="{appl_id}"]/{switch.actuator}/{switch.func_type}'
|
405
418
|
found = self._domain_objects.findall(locator)
|
@@ -412,39 +425,42 @@ class SmileAPI(SmileData):
|
|
412
425
|
else: # actuators with a single item like relay_functionality
|
413
426
|
switch_id = item.attrib["id"]
|
414
427
|
|
415
|
-
data = (
|
416
|
-
f"<{switch.func_type}>"
|
417
|
-
f"<{switch.func}>{state}</{switch.func}>"
|
418
|
-
f"</{switch.func_type}>"
|
419
|
-
)
|
420
428
|
uri = f"{APPLIANCES};id={appl_id}/{switch.device};id={switch_id}"
|
421
429
|
if model == "relay":
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
raise PlugwiseError("Plugwise: the locked Relay was not switched.")
|
430
|
+
lock_blocked = self.gw_entities[appl_id]["switches"].get("lock")
|
431
|
+
if lock_blocked or lock_blocked is None:
|
432
|
+
# Don't switch a relay when its corresponding lock-state is true or no
|
433
|
+
# lock is present. That means the relay can't be controlled by the user.
|
434
|
+
return current_state
|
428
435
|
|
429
436
|
await self.call_request(uri, method="put", data=data)
|
437
|
+
return requested_state
|
430
438
|
|
431
439
|
async def _set_groupswitch_member_state(
|
432
|
-
self, members: list[str], state: str, switch: Munch
|
433
|
-
) ->
|
440
|
+
self, appl_id: str, data: str, members: list[str], state: str, switch: Munch
|
441
|
+
) -> bool:
|
434
442
|
"""Helper-function for set_switch_state().
|
435
443
|
|
436
|
-
Set the
|
444
|
+
Set the requested state of the relevant switch within a group of switches.
|
445
|
+
Return the current group-state when none of the switches has changed its state, the requested state otherwise.
|
437
446
|
"""
|
447
|
+
current_state = self.gw_entities[appl_id]["switches"]["relay"]
|
448
|
+
requested_state = state == STATE_ON
|
449
|
+
switched = 0
|
438
450
|
for member in members:
|
439
451
|
locator = f'appliance[@id="{member}"]/{switch.actuator}/{switch.func_type}'
|
440
452
|
switch_id = self._domain_objects.find(locator).attrib["id"]
|
441
453
|
uri = f"{APPLIANCES};id={member}/{switch.device};id={switch_id}"
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
454
|
+
lock_blocked = self.gw_entities[member]["switches"].get("lock")
|
455
|
+
# Assume Plugs under Plugwise control are not part of a group
|
456
|
+
if lock_blocked is not None and not lock_blocked:
|
457
|
+
await self.call_request(uri, method="put", data=data)
|
458
|
+
switched += 1
|
459
|
+
|
460
|
+
if switched > 0:
|
461
|
+
return requested_state
|
462
|
+
|
463
|
+
return current_state
|
448
464
|
|
449
465
|
async def set_temperature(self, loc_id: str, items: dict[str, float]) -> None:
|
450
466
|
"""Set the given Temperature on the relevant Thermostat."""
|
@@ -453,7 +469,7 @@ class SmileAPI(SmileData):
|
|
453
469
|
if "setpoint" in items:
|
454
470
|
setpoint = items["setpoint"]
|
455
471
|
|
456
|
-
if self.
|
472
|
+
if self.check_name(ANNA) and self._cooling_present:
|
457
473
|
if "setpoint_high" not in items:
|
458
474
|
raise PlugwiseError(
|
459
475
|
"Plugwise: failed setting temperature: no valid input provided"
|
@@ -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
|
@@ -0,0 +1,18 @@
|
|
1
|
+
plugwise/__init__.py,sha256=tctQhmRTvr70Am9x5V1UZf_VtPlw6o2if1MlgMVGcYo,17408
|
2
|
+
plugwise/common.py,sha256=vP4DgD1_Sv5B-7MwSjiYn7xoke20oDWggQ3aY5W1Jeg,10084
|
3
|
+
plugwise/constants.py,sha256=V7piQ9xOrJ8nDK-RBcMiodSX3L-vEgRwnWmmxyRZCX8,16917
|
4
|
+
plugwise/data.py,sha256=ZS6Oo05R0vXksYwsReKQLGCoyQd8bo-hg8jGE2uMH-w,12495
|
5
|
+
plugwise/exceptions.py,sha256=Ce-tO9uNsMB-8FP6VAxBvsHNJ-NIM9F0onUZOdZI4Ys,1110
|
6
|
+
plugwise/helper.py,sha256=LmepTNlN_EtamwZ-ur8A-MWY04X4wG1ZPN-E1f9Y1W0,38908
|
7
|
+
plugwise/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
|
+
plugwise/smile.py,sha256=hk_KTOzajekJvxi05g4sJoJ49YvlSLLUiNluvMUbKBM,20319
|
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=vUuYX-vJErbVwGJu7On1ITDh8X8nzO8WoqN-whS9FJ4,16710
|
13
|
+
plugwise/legacy/smile.py,sha256=RBfasiOrAXUWwvUqciO0xzel4VMNsY0Nzb44GHqDPpo,12837
|
14
|
+
plugwise-1.7.7.dist-info/licenses/LICENSE,sha256=mL22BjmXtg_wnoDnnaqps5_Bg_VGj_yHueX5lsKwbCc,1144
|
15
|
+
plugwise-1.7.7.dist-info/METADATA,sha256=dDEkjG-sCbjz4Hscb23yRpRuCT8vZkCVXZvFy2IHOTQ,7903
|
16
|
+
plugwise-1.7.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
17
|
+
plugwise-1.7.7.dist-info/top_level.txt,sha256=MYOmktMFf8ZmX6_OE1y9MoCZFfY-L8DA0F2tA2IvE4s,9
|
18
|
+
plugwise-1.7.7.dist-info/RECORD,,
|
plugwise-1.7.5.dist-info/RECORD
DELETED
@@ -1,18 +0,0 @@
|
|
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.5.dist-info/licenses/LICENSE,sha256=mL22BjmXtg_wnoDnnaqps5_Bg_VGj_yHueX5lsKwbCc,1144
|
15
|
-
plugwise-1.7.5.dist-info/METADATA,sha256=UwtDKfDR3OtdpQ09Ra4KwD2xcQte8t9db-X81VRAkfw,7879
|
16
|
-
plugwise-1.7.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
17
|
-
plugwise-1.7.5.dist-info/top_level.txt,sha256=MYOmktMFf8ZmX6_OE1y9MoCZFfY-L8DA0F2tA2IvE4s,9
|
18
|
-
plugwise-1.7.5.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|