plugwise 0.36.2__py3-none-any.whl → 0.37.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 +187 -713
- plugwise/data.py +263 -0
- plugwise/helper.py +141 -381
- plugwise/legacy/data.py +124 -0
- plugwise/legacy/helper.py +775 -0
- plugwise/legacy/smile.py +272 -0
- plugwise/smile.py +426 -0
- plugwise/util.py +35 -1
- {plugwise-0.36.2.dist-info → plugwise-0.37.0.dist-info}/METADATA +2 -2
- plugwise-0.37.0.dist-info/RECORD +16 -0
- {plugwise-0.36.2.dist-info → plugwise-0.37.0.dist-info}/WHEEL +1 -1
- plugwise-0.36.2.dist-info/RECORD +0 -11
- {plugwise-0.36.2.dist-info → plugwise-0.37.0.dist-info}/LICENSE +0 -0
- {plugwise-0.36.2.dist-info → plugwise-0.37.0.dist-info}/top_level.txt +0 -0
plugwise/__init__.py
CHANGED
@@ -4,333 +4,39 @@ Plugwise backend module for Home Assistant Core.
|
|
4
4
|
"""
|
5
5
|
from __future__ import annotations
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
import aiohttp
|
10
|
-
from defusedxml import ElementTree as etree
|
11
|
-
|
12
|
-
# Dict as class
|
13
|
-
from munch import Munch
|
14
|
-
|
15
|
-
# Version detection
|
16
|
-
import semver
|
17
|
-
|
18
|
-
from .constants import (
|
19
|
-
ADAM,
|
20
|
-
ANNA,
|
21
|
-
APPLIANCES,
|
7
|
+
from plugwise.constants import (
|
22
8
|
DEFAULT_PORT,
|
23
9
|
DEFAULT_TIMEOUT,
|
24
10
|
DEFAULT_USERNAME,
|
25
11
|
DOMAIN_OBJECTS,
|
26
|
-
LOCATIONS,
|
27
12
|
LOGGER,
|
28
|
-
MAX_SETPOINT,
|
29
|
-
MIN_SETPOINT,
|
30
13
|
MODULES,
|
31
14
|
NONE,
|
32
|
-
NOTIFICATIONS,
|
33
|
-
OFF,
|
34
|
-
REQUIRE_APPLIANCES,
|
35
|
-
RULES,
|
36
15
|
SMILES,
|
37
16
|
STATUS,
|
38
|
-
SWITCH_GROUP_TYPES,
|
39
17
|
SYSTEM,
|
40
|
-
ZONE_THERMOSTATS,
|
41
|
-
ActuatorData,
|
42
|
-
DeviceData,
|
43
|
-
GatewayData,
|
44
18
|
PlugwiseData,
|
19
|
+
ThermoLoc,
|
45
20
|
)
|
46
|
-
from .exceptions import (
|
21
|
+
from plugwise.exceptions import (
|
47
22
|
InvalidSetupError,
|
48
23
|
PlugwiseError,
|
49
24
|
ResponseError,
|
50
25
|
UnsupportedDeviceError,
|
51
26
|
)
|
52
|
-
from .helper import SmileComm
|
53
|
-
|
54
|
-
|
55
|
-
def remove_empty_platform_dicts(data: DeviceData) -> None:
|
56
|
-
"""Helper-function for removing any empty platform dicts."""
|
57
|
-
if not data["binary_sensors"]:
|
58
|
-
data.pop("binary_sensors")
|
59
|
-
if not data["sensors"]:
|
60
|
-
data.pop("sensors")
|
61
|
-
if not data["switches"]:
|
62
|
-
data.pop("switches")
|
63
|
-
|
64
|
-
|
65
|
-
class SmileData(SmileHelper):
|
66
|
-
"""The Plugwise Smile main class."""
|
67
|
-
|
68
|
-
def _update_gw_devices(self) -> None:
|
69
|
-
"""Helper-function for _all_device_data() and async_update().
|
70
|
-
|
71
|
-
Collect data for each device and add to self.gw_devices.
|
72
|
-
"""
|
73
|
-
for device_id, device in self.gw_devices.items():
|
74
|
-
data = self._get_device_data(device_id)
|
75
|
-
self._add_or_update_notifications(device_id, device, data)
|
76
|
-
device.update(data)
|
77
|
-
self._update_for_cooling(device)
|
78
|
-
remove_empty_platform_dicts(device)
|
79
|
-
|
80
|
-
def _add_or_update_notifications(
|
81
|
-
self, device_id: str, device: DeviceData, data: DeviceData
|
82
|
-
) -> None:
|
83
|
-
"""Helper-function adding or updating the Plugwise notifications."""
|
84
|
-
if (
|
85
|
-
device_id == self.gateway_id
|
86
|
-
and (
|
87
|
-
self._is_thermostat
|
88
|
-
or (self.smile_type == "power" and not self._smile_legacy)
|
89
|
-
)
|
90
|
-
) or (
|
91
|
-
"binary_sensors" in device
|
92
|
-
and "plugwise_notification" in device["binary_sensors"]
|
93
|
-
):
|
94
|
-
data["binary_sensors"]["plugwise_notification"] = bool(self._notifications)
|
95
|
-
self._count += 1
|
96
|
-
|
97
|
-
def _update_for_cooling(self, device: DeviceData) -> None:
|
98
|
-
"""Helper-function for adding/updating various cooling-related values."""
|
99
|
-
# For Anna and heating + cooling, replace setpoint with setpoint_high/_low
|
100
|
-
if (
|
101
|
-
self.smile(ANNA)
|
102
|
-
and self._cooling_present
|
103
|
-
and device["dev_class"] == "thermostat"
|
104
|
-
):
|
105
|
-
thermostat = device["thermostat"]
|
106
|
-
sensors = device["sensors"]
|
107
|
-
temp_dict: ActuatorData = {
|
108
|
-
"setpoint_low": thermostat["setpoint"],
|
109
|
-
"setpoint_high": MAX_SETPOINT,
|
110
|
-
}
|
111
|
-
if self._cooling_enabled:
|
112
|
-
temp_dict = {
|
113
|
-
"setpoint_low": MIN_SETPOINT,
|
114
|
-
"setpoint_high": thermostat["setpoint"],
|
115
|
-
}
|
116
|
-
thermostat.pop("setpoint")
|
117
|
-
temp_dict.update(thermostat)
|
118
|
-
device["thermostat"] = temp_dict
|
119
|
-
if "setpoint" in sensors:
|
120
|
-
sensors.pop("setpoint")
|
121
|
-
sensors["setpoint_low"] = temp_dict["setpoint_low"]
|
122
|
-
sensors["setpoint_high"] = temp_dict["setpoint_high"]
|
123
|
-
self._count += 2
|
124
|
-
|
125
|
-
def get_all_devices(self) -> None:
|
126
|
-
"""Determine the evices present from the obtained XML-data.
|
127
|
-
|
128
|
-
Run this functions once to gather the initial device configuration,
|
129
|
-
then regularly run async_update() to refresh the device data.
|
130
|
-
"""
|
131
|
-
# Gather all the devices and their initial data
|
132
|
-
self._all_appliances()
|
133
|
-
if self._is_thermostat:
|
134
|
-
self._scan_thermostats()
|
135
|
-
# Collect a list of thermostats with offset-capability
|
136
|
-
self.therms_with_offset_func = (
|
137
|
-
self._get_appliances_with_offset_functionality()
|
138
|
-
)
|
139
|
-
|
140
|
-
# Collect and add switching- and/or pump-group devices
|
141
|
-
if group_data := self._get_group_switches():
|
142
|
-
self.gw_devices.update(group_data)
|
143
|
-
|
144
|
-
# Collect the remaining data for all devices
|
145
|
-
self._all_device_data()
|
146
|
-
|
147
|
-
def _all_device_data(self) -> None:
|
148
|
-
"""Helper-function for get_all_devices().
|
149
|
-
|
150
|
-
Collect data for each device and add to self.gw_data and self.gw_devices.
|
151
|
-
"""
|
152
|
-
self._update_gw_devices()
|
153
|
-
self.device_items = self._count
|
154
|
-
self.device_list = []
|
155
|
-
for device in self.gw_devices:
|
156
|
-
self.device_list.append(device)
|
157
|
-
|
158
|
-
self.gw_data.update(
|
159
|
-
{
|
160
|
-
"gateway_id": self.gateway_id,
|
161
|
-
"item_count": self._count,
|
162
|
-
"notifications": self._notifications,
|
163
|
-
"smile_name": self.smile_name,
|
164
|
-
}
|
165
|
-
)
|
166
|
-
if self._is_thermostat:
|
167
|
-
self.gw_data.update(
|
168
|
-
{"heater_id": self._heater_id, "cooling_present": self._cooling_present}
|
169
|
-
)
|
170
|
-
|
171
|
-
def _device_data_switching_group(
|
172
|
-
self, device: DeviceData, data: DeviceData
|
173
|
-
) -> None:
|
174
|
-
"""Helper-function for _get_device_data().
|
175
|
-
|
176
|
-
Determine switching group device data.
|
177
|
-
"""
|
178
|
-
if device["dev_class"] in SWITCH_GROUP_TYPES:
|
179
|
-
counter = 0
|
180
|
-
for member in device["members"]:
|
181
|
-
if self.gw_devices[member]["switches"].get("relay"):
|
182
|
-
counter += 1
|
183
|
-
data["switches"]["relay"] = counter != 0
|
184
|
-
self._count += 1
|
185
|
-
|
186
|
-
def _device_data_adam(self, device: DeviceData, data: DeviceData) -> None:
|
187
|
-
"""Helper-function for _get_device_data().
|
188
|
-
|
189
|
-
Determine Adam heating-status for on-off heating via valves,
|
190
|
-
available regulations_modes and thermostat control_states.
|
191
|
-
"""
|
192
|
-
if self.smile(ADAM):
|
193
|
-
# Indicate heating_state based on valves being open in case of city-provided heating
|
194
|
-
if (
|
195
|
-
device["dev_class"] == "heater_central"
|
196
|
-
and self._on_off_device
|
197
|
-
and isinstance(self._heating_valves(), int)
|
198
|
-
):
|
199
|
-
data["binary_sensors"]["heating_state"] = self._heating_valves() != 0
|
200
|
-
|
201
|
-
# Show the allowed regulation modes and gateway_modes
|
202
|
-
if device["dev_class"] == "gateway":
|
203
|
-
if self._reg_allowed_modes:
|
204
|
-
data["regulation_modes"] = self._reg_allowed_modes
|
205
|
-
self._count += 1
|
206
|
-
if self._gw_allowed_modes:
|
207
|
-
data["gateway_modes"] = self._gw_allowed_modes
|
208
|
-
self._count += 1
|
209
|
-
|
210
|
-
# Control_state, only for Adam master thermostats
|
211
|
-
if device["dev_class"] in ZONE_THERMOSTATS:
|
212
|
-
loc_id = device["location"]
|
213
|
-
if ctrl_state := self._control_state(loc_id):
|
214
|
-
data["control_state"] = ctrl_state
|
215
|
-
self._count += 1
|
216
|
-
|
217
|
-
def _device_data_climate(self, device: DeviceData, data: DeviceData) -> None:
|
218
|
-
"""Helper-function for _get_device_data().
|
219
|
-
|
220
|
-
Determine climate-control device data.
|
221
|
-
"""
|
222
|
-
loc_id = device["location"]
|
223
|
-
|
224
|
-
# Presets
|
225
|
-
data["preset_modes"] = None
|
226
|
-
data["active_preset"] = None
|
227
|
-
self._count += 2
|
228
|
-
if presets := self._presets(loc_id):
|
229
|
-
data["preset_modes"] = list(presets)
|
230
|
-
data["active_preset"] = self._preset(loc_id)
|
231
|
-
|
232
|
-
# Schedule
|
233
|
-
avail_schedules, sel_schedule = self._schedules(loc_id)
|
234
|
-
data["available_schedules"] = avail_schedules
|
235
|
-
data["select_schedule"] = sel_schedule
|
236
|
-
self._count += 2
|
237
|
-
|
238
|
-
# Operation modes: auto, heat, heat_cool, cool and off
|
239
|
-
data["mode"] = "auto"
|
240
|
-
self._count += 1
|
241
|
-
if sel_schedule == NONE:
|
242
|
-
data["mode"] = "heat"
|
243
|
-
if self._cooling_present:
|
244
|
-
data["mode"] = "cool" if self.check_reg_mode("cooling") else "heat_cool"
|
245
|
-
|
246
|
-
if self.check_reg_mode("off"):
|
247
|
-
data["mode"] = "off"
|
248
|
-
|
249
|
-
if NONE not in avail_schedules:
|
250
|
-
self._get_schedule_states_with_off(
|
251
|
-
loc_id, avail_schedules, sel_schedule, data
|
252
|
-
)
|
253
|
-
|
254
|
-
def check_reg_mode(self, mode: str) -> bool:
|
255
|
-
"""Helper-function for device_data_climate()."""
|
256
|
-
gateway = self.gw_devices[self.gateway_id]
|
257
|
-
return (
|
258
|
-
"regulation_modes" in gateway and gateway["select_regulation_mode"] == mode
|
259
|
-
)
|
27
|
+
from plugwise.helper import SmileComm
|
28
|
+
from plugwise.legacy.smile import SmileLegacyAPI
|
29
|
+
from plugwise.smile import SmileAPI
|
260
30
|
|
261
|
-
|
262
|
-
|
263
|
-
) -> None:
|
264
|
-
"""Collect schedules with states for each thermostat.
|
265
|
-
|
266
|
-
Also, replace NONE by OFF when none of the schedules are active,
|
267
|
-
only for non-legacy thermostats.
|
268
|
-
"""
|
269
|
-
loc_schedule_states: dict[str, str] = {}
|
270
|
-
for schedule in schedules:
|
271
|
-
loc_schedule_states[schedule] = "off"
|
272
|
-
if schedule == selected and data["mode"] == "auto":
|
273
|
-
loc_schedule_states[schedule] = "on"
|
274
|
-
self._schedule_old_states[location] = loc_schedule_states
|
275
|
-
|
276
|
-
all_off = True
|
277
|
-
if not self._smile_legacy:
|
278
|
-
for state in self._schedule_old_states[location].values():
|
279
|
-
if state == "on":
|
280
|
-
all_off = False
|
281
|
-
if all_off:
|
282
|
-
data["select_schedule"] = OFF
|
283
|
-
|
284
|
-
def _check_availability(
|
285
|
-
self, device: DeviceData, dev_class: str, data: DeviceData, message: str
|
286
|
-
) -> None:
|
287
|
-
"""Helper-function for _get_device_data().
|
288
|
-
|
289
|
-
Provide availability status for the wired-commected devices.
|
290
|
-
"""
|
291
|
-
if device["dev_class"] == dev_class:
|
292
|
-
data["available"] = True
|
293
|
-
self._count += 1
|
294
|
-
for item in self._notifications.values():
|
295
|
-
for msg in item.values():
|
296
|
-
if message in msg:
|
297
|
-
data["available"] = False
|
298
|
-
|
299
|
-
def _get_device_data(self, dev_id: str) -> DeviceData:
|
300
|
-
"""Helper-function for _all_device_data() and async_update().
|
301
|
-
|
302
|
-
Provide device-data, based on Location ID (= dev_id), from APPLIANCES.
|
303
|
-
"""
|
304
|
-
device = self.gw_devices[dev_id]
|
305
|
-
data = self._get_measurement_data(dev_id)
|
306
|
-
|
307
|
-
# Check availability of non-legacy wired-connected devices
|
308
|
-
if not self._smile_legacy:
|
309
|
-
# Smartmeter
|
310
|
-
self._check_availability(
|
311
|
-
device, "smartmeter", data, "P1 does not seem to be connected"
|
312
|
-
)
|
313
|
-
# OpenTherm device
|
314
|
-
if device["name"] != "OnOff":
|
315
|
-
self._check_availability(
|
316
|
-
device, "heater_central", data, "no OpenTherm communication"
|
317
|
-
)
|
318
|
-
|
319
|
-
# Switching groups data
|
320
|
-
self._device_data_switching_group(device, data)
|
321
|
-
# Adam data
|
322
|
-
self._device_data_adam(device, data)
|
323
|
-
# Skip obtaining data for non master-thermostats
|
324
|
-
if device["dev_class"] not in ZONE_THERMOSTATS:
|
325
|
-
return data
|
326
|
-
|
327
|
-
# Thermostat data (presets, temperatures etc)
|
328
|
-
self._device_data_climate(device, data)
|
31
|
+
import aiohttp
|
32
|
+
from defusedxml import ElementTree as etree
|
329
33
|
|
330
|
-
|
34
|
+
# Dict as class
|
35
|
+
# Version detection
|
36
|
+
import semver
|
331
37
|
|
332
38
|
|
333
|
-
class Smile(SmileComm
|
39
|
+
class Smile(SmileComm):
|
334
40
|
"""The Plugwise SmileConnect class."""
|
335
41
|
|
336
42
|
# pylint: disable=too-many-instance-attributes, too-many-public-methods
|
@@ -339,30 +45,56 @@ class Smile(SmileComm, SmileData):
|
|
339
45
|
self,
|
340
46
|
host: str,
|
341
47
|
password: str,
|
48
|
+
websession: aiohttp.ClientSession,
|
342
49
|
username: str = DEFAULT_USERNAME,
|
343
50
|
port: int = DEFAULT_PORT,
|
344
51
|
timeout: float = DEFAULT_TIMEOUT,
|
345
|
-
|
52
|
+
|
346
53
|
) -> None:
|
347
54
|
"""Set the constructor for this class."""
|
348
55
|
super().__init__(
|
349
56
|
host,
|
350
57
|
password,
|
58
|
+
websession,
|
351
59
|
username,
|
352
60
|
port,
|
353
61
|
timeout,
|
354
|
-
websession,
|
355
62
|
)
|
356
|
-
SmileData.__init__(self)
|
357
63
|
|
358
|
-
self.
|
359
|
-
self.
|
360
|
-
self.
|
64
|
+
self._host = host
|
65
|
+
self._passwd = password
|
66
|
+
self._websession = websession
|
67
|
+
self._user = username
|
68
|
+
self._port = port
|
69
|
+
self._timeout = timeout
|
70
|
+
|
71
|
+
self._cooling_present = False
|
72
|
+
self._elga = False
|
73
|
+
self._is_thermostat = False
|
74
|
+
self._last_active: dict[str, str | None] = {}
|
75
|
+
self._on_off_device = False
|
76
|
+
self._opentherm_device = False
|
77
|
+
self._schedule_old_states: dict[str, dict[str, str]] = {}
|
78
|
+
self._smile_api: SmileAPI | SmileLegacyAPI
|
79
|
+
self._stretch_v2 = False
|
80
|
+
self._target_smile: str = NONE
|
81
|
+
self.gateway_id: str = NONE
|
82
|
+
self.loc_data: dict[str, ThermoLoc] = {}
|
83
|
+
self.smile_fw_version: str | None
|
84
|
+
self.smile_hostname: str
|
85
|
+
self.smile_hw_version: str | None = None
|
86
|
+
self.smile_legacy = False
|
87
|
+
self.smile_mac_address: str | None
|
88
|
+
self.smile_model: str
|
89
|
+
self.smile_name: str
|
90
|
+
self.smile_type: str
|
91
|
+
self.smile_version: tuple[str, semver.version.Version]
|
92
|
+
self.smile_zigbee_mac_address: str | None = None
|
361
93
|
|
362
94
|
async def connect(self) -> bool:
|
363
95
|
"""Connect to Plugwise device and determine its name, type and version."""
|
364
96
|
result = await self._request(DOMAIN_OBJECTS)
|
365
|
-
# Work-around for Stretch
|
97
|
+
# Work-around for Stretch fw 2.7.18
|
366
98
|
if not (vendor_names := result.findall("./module/vendor_name")):
|
367
99
|
result = await self._request(MODULES)
|
368
100
|
vendor_names = result.findall("./module/vendor_name")
|
@@ -395,58 +127,62 @@ class Smile(SmileComm, SmileData):
|
|
395
127
|
# Determine smile specifics
|
396
128
|
await self._smile_detect(result, dsmrmain)
|
397
129
|
|
130
|
+
self._smile_api = SmileAPI(
|
131
|
+
self._host,
|
132
|
+
self._passwd,
|
133
|
+
self._websession,
|
134
|
+
self._cooling_present,
|
135
|
+
self._elga,
|
136
|
+
self._is_thermostat,
|
137
|
+
self._last_active,
|
138
|
+
self._on_off_device,
|
139
|
+
self._opentherm_device,
|
140
|
+
self._schedule_old_states,
|
141
|
+
self.gateway_id,
|
142
|
+
self.loc_data,
|
143
|
+
self.smile_fw_version,
|
144
|
+
self.smile_hostname,
|
145
|
+
self.smile_hw_version,
|
146
|
+
self.smile_legacy,
|
147
|
+
self.smile_mac_address,
|
148
|
+
self.smile_model,
|
149
|
+
self.smile_name,
|
150
|
+
self.smile_type,
|
151
|
+
self.smile_version,
|
152
|
+
self._user,
|
153
|
+
self._port,
|
154
|
+
self._timeout,
|
155
|
+
)
|
156
|
+
if self.smile_legacy:
|
157
|
+
self._smile_api = SmileLegacyAPI(
|
158
|
+
self._host,
|
159
|
+
self._passwd,
|
160
|
+
self._websession,
|
161
|
+
self._is_thermostat,
|
162
|
+
self._on_off_device,
|
163
|
+
self._opentherm_device,
|
164
|
+
self._stretch_v2,
|
165
|
+
self._target_smile,
|
166
|
+
self.loc_data,
|
167
|
+
self.smile_fw_version,
|
168
|
+
self.smile_hostname,
|
169
|
+
self.smile_hw_version,
|
170
|
+
self.smile_mac_address,
|
171
|
+
self.smile_model,
|
172
|
+
self.smile_name,
|
173
|
+
self.smile_type,
|
174
|
+
self.smile_version,
|
175
|
+
self.smile_zigbee_mac_address,
|
176
|
+
self._user,
|
177
|
+
self._port,
|
178
|
+
self._timeout,
|
179
|
+
)
|
180
|
+
|
398
181
|
# Update all endpoints on first connect
|
399
|
-
await self.
|
182
|
+
await self._smile_api.full_update_device()
|
400
183
|
|
401
184
|
return True
|
402
185
|
|
403
|
-
async def _smile_detect_legacy(
|
404
|
-
self, result: etree, dsmrmain: etree, model: str
|
405
|
-
) -> str:
|
406
|
-
"""Helper-function for _smile_detect()."""
|
407
|
-
return_model = model
|
408
|
-
# Stretch: find the MAC of the zigbee master_controller (= Stick)
|
409
|
-
if (network := result.find("./module/protocols/master_controller")) is not None:
|
410
|
-
self.smile_zigbee_mac_address = network.find("mac_address").text
|
411
|
-
# Find the active MAC in case there is an orphaned Stick
|
412
|
-
if zb_networks := result.findall("./network"):
|
413
|
-
for zb_network in zb_networks:
|
414
|
-
if zb_network.find("./nodes/network_router") is not None:
|
415
|
-
network = zb_network.find("./master_controller")
|
416
|
-
self.smile_zigbee_mac_address = network.find("mac_address").text
|
417
|
-
|
418
|
-
# Legacy Anna or Stretch:
|
419
|
-
if (
|
420
|
-
result.find('./appliance[type="thermostat"]') is not None
|
421
|
-
or network is not None
|
422
|
-
):
|
423
|
-
self._system = await self._request(SYSTEM)
|
424
|
-
self.smile_fw_version = self._system.find("./gateway/firmware").text
|
425
|
-
return_model = self._system.find("./gateway/product").text
|
426
|
-
self.smile_hostname = self._system.find("./gateway/hostname").text
|
427
|
-
# If wlan0 contains data it's active, so eth0 should be checked last
|
428
|
-
for network in ("wlan0", "eth0"):
|
429
|
-
locator = f"./{network}/mac"
|
430
|
-
if (net_locator := self._system.find(locator)) is not None:
|
431
|
-
self.smile_mac_address = net_locator.text
|
432
|
-
# P1 legacy:
|
433
|
-
elif dsmrmain is not None:
|
434
|
-
self._status = await self._request(STATUS)
|
435
|
-
self.smile_fw_version = self._status.find("./system/version").text
|
436
|
-
return_model = self._status.find("./system/product").text
|
437
|
-
self.smile_hostname = self._status.find("./network/hostname").text
|
438
|
-
self.smile_mac_address = self._status.find("./network/mac_address").text
|
439
|
-
else: # pragma: no cover
|
440
|
-
# No cornercase, just end of the line
|
441
|
-
LOGGER.error(
|
442
|
-
"Connected but no gateway device information found, please create"
|
443
|
-
" an issue on http://github.com/plugwise/python-plugwise"
|
444
|
-
)
|
445
|
-
raise ResponseError
|
446
|
-
|
447
|
-
self._smile_legacy = True
|
448
|
-
return return_model
|
449
|
-
|
450
186
|
async def _smile_detect(self, result: etree, dsmrmain: etree) -> None:
|
451
187
|
"""Helper-function for connect().
|
452
188
|
|
@@ -496,7 +232,6 @@ class Smile(SmileComm, SmileData):
|
|
496
232
|
|
497
233
|
if self.smile_type == "stretch":
|
498
234
|
self._stretch_v2 = self.smile_version[1].major == 2
|
499
|
-
self._stretch_v3 = self.smile_version[1].major == 3
|
500
235
|
|
501
236
|
if self.smile_type == "thermostat":
|
502
237
|
self._is_thermostat = True
|
@@ -518,119 +253,70 @@ class Smile(SmileComm, SmileData):
|
|
518
253
|
if result.find(locator_2) is not None:
|
519
254
|
self._elga = True
|
520
255
|
|
521
|
-
async def
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
try:
|
536
|
-
msg_id = notification.attrib["id"]
|
537
|
-
msg_type = notification.find("type").text
|
538
|
-
msg = notification.find("message").text
|
539
|
-
self._notifications.update({msg_id: {msg_type: msg}})
|
540
|
-
LOGGER.debug("Plugwise notifications: %s", self._notifications)
|
541
|
-
except AttributeError: # pragma: no cover
|
542
|
-
LOGGER.debug(
|
543
|
-
"Plugwise notification present but unable to process, manually investigate: %s",
|
544
|
-
f"{self._endpoint}{DOMAIN_OBJECTS}",
|
545
|
-
)
|
256
|
+
async def _smile_detect_legacy(
|
257
|
+
self, result: etree, dsmrmain: etree, model: str
|
258
|
+
) -> str:
|
259
|
+
"""Helper-function for _smile_detect()."""
|
260
|
+
return_model = model
|
261
|
+
# Stretch: find the MAC of the zigbee master_controller (= Stick)
|
262
|
+
if (network := result.find("./module/protocols/master_controller")) is not None:
|
263
|
+
self.smile_zigbee_mac_address = network.find("mac_address").text
|
264
|
+
# Find the active MAC in case there is an orphaned Stick
|
265
|
+
if zb_networks := result.findall("./network"):
|
266
|
+
for zb_network in zb_networks:
|
267
|
+
if zb_network.find("./nodes/network_router") is not None:
|
268
|
+
network = zb_network.find("./master_controller")
|
269
|
+
self.smile_zigbee_mac_address = network.find("mac_address").text
|
546
270
|
|
547
|
-
|
548
|
-
"""Perform an incremental update for updating the various device states."""
|
549
|
-
# Perform a full update at day-change
|
550
|
-
day_number = dt.datetime.now().strftime("%w")
|
271
|
+
# Legacy Anna or Stretch:
|
551
272
|
if (
|
552
|
-
|
553
|
-
|
273
|
+
result.find('./appliance[type="thermostat"]') is not None
|
274
|
+
or network is not None
|
554
275
|
):
|
555
|
-
|
556
|
-
|
276
|
+
system = await self._request(SYSTEM)
|
277
|
+
self.smile_fw_version = system.find("./gateway/firmware").text
|
278
|
+
return_model = system.find("./gateway/product").text
|
279
|
+
self.smile_hostname = system.find("./gateway/hostname").text
|
280
|
+
# If wlan0 contains data it's active, so eth0 should be checked last
|
281
|
+
for network in ("wlan0", "eth0"):
|
282
|
+
locator = f"./{network}/mac"
|
283
|
+
if (net_locator := system.find(locator)) is not None:
|
284
|
+
self.smile_mac_address = net_locator.text
|
285
|
+
# P1 legacy:
|
286
|
+
elif dsmrmain is not None:
|
287
|
+
status = await self._request(STATUS)
|
288
|
+
self.smile_fw_version = status.find("./system/version").text
|
289
|
+
return_model = status.find("./system/product").text
|
290
|
+
self.smile_hostname = status.find("./network/hostname").text
|
291
|
+
self.smile_mac_address = status.find("./network/mac_address").text
|
292
|
+
else: # pragma: no cover
|
293
|
+
# No cornercase, just end of the line
|
294
|
+
LOGGER.error(
|
295
|
+
"Connected but no gateway device information found, please create"
|
296
|
+
" an issue on http://github.com/plugwise/python-plugwise"
|
557
297
|
)
|
558
|
-
|
559
|
-
self.gw_devices: dict[str, DeviceData] = {}
|
560
|
-
await self._full_update_device()
|
561
|
-
self.get_all_devices()
|
562
|
-
# Otherwise perform an incremental update
|
563
|
-
else:
|
564
|
-
self._domain_objects = await self._request(DOMAIN_OBJECTS)
|
565
|
-
self._get_plugwise_notifications()
|
566
|
-
match self._target_smile:
|
567
|
-
case "smile_v2":
|
568
|
-
self._modules = await self._request(MODULES)
|
569
|
-
case "smile_v3" | "smile_v4":
|
570
|
-
self._locations = await self._request(LOCATIONS)
|
571
|
-
case "smile_open_therm_v2" | "smile_open_therm_v3":
|
572
|
-
self._appliances = await self._request(APPLIANCES)
|
573
|
-
self._modules = await self._request(MODULES)
|
574
|
-
case self._target_smile if self._target_smile in REQUIRE_APPLIANCES:
|
575
|
-
self._appliances = await self._request(APPLIANCES)
|
576
|
-
|
577
|
-
self._update_gw_devices()
|
578
|
-
self.gw_data["notifications"] = self._notifications
|
579
|
-
|
580
|
-
self._previous_day_number = day_number
|
581
|
-
return PlugwiseData(self.gw_data, self.gw_devices)
|
582
|
-
|
583
|
-
async def _set_schedule_state_legacy(
|
584
|
-
self, loc_id: str, name: str, status: str
|
585
|
-
) -> None:
|
586
|
-
"""Helper-function for set_schedule_state()."""
|
587
|
-
schedule_rule_id: str | None = None
|
588
|
-
for rule in self._domain_objects.findall("rule"):
|
589
|
-
if rule.find("name").text == name:
|
590
|
-
schedule_rule_id = rule.attrib["id"]
|
591
|
-
|
592
|
-
if schedule_rule_id is None:
|
593
|
-
raise PlugwiseError("Plugwise: no schedule with this name available.")
|
594
|
-
|
595
|
-
new_state = "false"
|
596
|
-
if status == "on":
|
597
|
-
new_state = "true"
|
598
|
-
# If no state change is requested, do nothing
|
599
|
-
if new_state == self._schedule_old_states[loc_id][name]:
|
600
|
-
return
|
601
|
-
|
602
|
-
locator = f'.//*[@id="{schedule_rule_id}"]/template'
|
603
|
-
for rule in self._domain_objects.findall(locator):
|
604
|
-
template_id = rule.attrib["id"]
|
605
|
-
|
606
|
-
uri = f"{RULES};id={schedule_rule_id}"
|
607
|
-
data = (
|
608
|
-
"<rules><rule"
|
609
|
-
f' id="{schedule_rule_id}"><name><![CDATA[{name}]]></name><template'
|
610
|
-
f' id="{template_id}" /><active>{new_state}</active></rule></rules>'
|
611
|
-
)
|
298
|
+
raise ResponseError
|
612
299
|
|
613
|
-
|
614
|
-
|
300
|
+
self.smile_legacy = True
|
301
|
+
return return_model
|
615
302
|
|
616
|
-
def
|
617
|
-
|
618
|
-
|
619
|
-
"""Helper-function for set_schedule_state()."""
|
620
|
-
locator = f'.//*[@id="{sched_id}"]/contexts'
|
621
|
-
contexts = self._domain_objects.find(locator)
|
622
|
-
locator = f'.//*[@id="{loc_id}"].../...'
|
623
|
-
if (subject := contexts.find(locator)) is None:
|
624
|
-
subject = f'<context><zone><location id="{loc_id}" /></zone></context>'
|
625
|
-
subject = etree.fromstring(subject)
|
303
|
+
async def full_update_device(self) -> None:
|
304
|
+
"""Perform a first fetch of all XML data, needed for initialization."""
|
305
|
+
await self._smile_api.full_update_device()
|
626
306
|
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
if state == "on":
|
631
|
-
contexts.append(subject)
|
307
|
+
def get_all_devices(self) -> None:
|
308
|
+
"""Determine the devices present from the obtained XML-data."""
|
309
|
+
self._smile_api.get_all_devices()
|
632
310
|
|
633
|
-
|
311
|
+
async def async_update(self) -> PlugwiseData:
|
312
|
+
"""Perform an incremental update for updating the various device states."""
|
313
|
+
data: PlugwiseData = await self._smile_api.async_update()
|
314
|
+
self.gateway_id = data.gateway["gateway_id"]
|
315
|
+
return data
|
316
|
+
|
317
|
+
########################################################################################################
|
318
|
+
### API Set and HA Service-related Functions ###
|
319
|
+
########################################################################################################
|
634
320
|
|
635
321
|
async def set_schedule_state(
|
636
322
|
self,
|
@@ -638,280 +324,68 @@ class Smile(SmileComm, SmileData):
|
|
638
324
|
new_state: str,
|
639
325
|
name: str | None = None,
|
640
326
|
) -> None:
|
641
|
-
"""Activate/deactivate the Schedule, with the given name, on the relevant Thermostat.
|
642
|
-
|
643
|
-
Determined from - DOMAIN_OBJECTS.
|
644
|
-
Used in HA Core to set the hvac_mode: in practice switch between schedule on - off.
|
645
|
-
"""
|
646
|
-
# Input checking
|
647
|
-
if new_state not in ["on", "off"]:
|
648
|
-
raise PlugwiseError("Plugwise: invalid schedule state.")
|
649
|
-
|
650
|
-
# Translate selection of Off-schedule-option to disabling the active schedule
|
651
|
-
if name == OFF:
|
652
|
-
new_state = "off"
|
653
|
-
|
654
|
-
# Handle no schedule-name / Off-schedule provided
|
655
|
-
if name is None or name == OFF:
|
656
|
-
if schedule_name := self._last_active[loc_id]:
|
657
|
-
name = schedule_name
|
658
|
-
else:
|
659
|
-
return
|
660
|
-
|
661
|
-
assert isinstance(name, str)
|
662
|
-
if self._smile_legacy:
|
663
|
-
await self._set_schedule_state_legacy(loc_id, name, new_state)
|
664
|
-
return
|
665
|
-
|
666
|
-
schedule_rule = self._rule_ids_by_name(name, loc_id)
|
667
|
-
# Raise an error when the schedule name does not exist
|
668
|
-
if not schedule_rule or schedule_rule is None:
|
669
|
-
raise PlugwiseError("Plugwise: no schedule with this name available.")
|
670
|
-
|
671
|
-
# If no state change is requested, do nothing
|
672
|
-
if new_state == self._schedule_old_states[loc_id][name]:
|
673
|
-
return
|
674
|
-
|
675
|
-
schedule_rule_id: str = next(iter(schedule_rule))
|
676
|
-
template = (
|
677
|
-
'<template tag="zone_preset_based_on_time_and_presence_with_override" />'
|
678
|
-
)
|
679
|
-
if self.smile(ANNA):
|
680
|
-
locator = f'.//*[@id="{schedule_rule_id}"]/template'
|
681
|
-
template_id = self._domain_objects.find(locator).attrib["id"]
|
682
|
-
template = f'<template id="{template_id}" />'
|
683
|
-
|
684
|
-
contexts = self.determine_contexts(loc_id, name, new_state, schedule_rule_id)
|
685
|
-
uri = f"{RULES};id={schedule_rule_id}"
|
686
|
-
data = (
|
687
|
-
f'<rules><rule id="{schedule_rule_id}"><name><![CDATA[{name}]]></name>'
|
688
|
-
f"{template}{contexts}</rule></rules>"
|
689
|
-
)
|
690
|
-
|
691
|
-
await self._request(uri, method="put", data=data)
|
692
|
-
self._schedule_old_states[loc_id][name] = new_state
|
693
|
-
|
694
|
-
async def _set_preset_legacy(self, preset: str) -> None:
|
695
|
-
"""Set the given Preset on the relevant Thermostat - from DOMAIN_OBJECTS."""
|
696
|
-
locator = f'rule/directives/when/then[@icon="{preset}"].../.../...'
|
697
|
-
rule = self._domain_objects.find(locator)
|
698
|
-
data = f'<rules><rule id="{rule.attrib["id"]}"><active>true</active></rule></rules>'
|
699
|
-
|
700
|
-
await self._request(RULES, method="put", data=data)
|
327
|
+
"""Activate/deactivate the Schedule, with the given name, on the relevant Thermostat."""
|
328
|
+
await self._smile_api.set_schedule_state(loc_id, new_state, name)
|
701
329
|
|
702
330
|
async def set_preset(self, loc_id: str, preset: str) -> None:
|
703
|
-
"""Set the given Preset on the relevant Thermostat
|
704
|
-
|
705
|
-
raise PlugwiseError("Plugwise: no presets available.") # pragma: no cover
|
706
|
-
if preset not in list(presets):
|
707
|
-
raise PlugwiseError("Plugwise: invalid preset.")
|
708
|
-
|
709
|
-
if self._smile_legacy:
|
710
|
-
await self._set_preset_legacy(preset)
|
711
|
-
return
|
712
|
-
|
713
|
-
current_location = self._locations.find(f'location[@id="{loc_id}"]')
|
714
|
-
location_name = current_location.find("name").text
|
715
|
-
location_type = current_location.find("type").text
|
716
|
-
|
717
|
-
uri = f"{LOCATIONS};id={loc_id}"
|
718
|
-
data = (
|
719
|
-
"<locations><location"
|
720
|
-
f' id="{loc_id}"><name>{location_name}</name><type>{location_type}'
|
721
|
-
f"</type><preset>{preset}</preset></location></locations>"
|
722
|
-
)
|
723
|
-
|
724
|
-
await self._request(uri, method="put", data=data)
|
331
|
+
"""Set the given Preset on the relevant Thermostat."""
|
332
|
+
await self._smile_api.set_preset(loc_id, preset)
|
725
333
|
|
726
334
|
async def set_temperature(self, loc_id: str, items: dict[str, float]) -> None:
|
727
335
|
"""Set the given Temperature on the relevant Thermostat."""
|
728
|
-
|
729
|
-
|
730
|
-
|
731
|
-
setpoint = items["setpoint"]
|
732
|
-
|
733
|
-
if self.smile(ANNA) and self._cooling_present:
|
734
|
-
if "setpoint_high" not in items:
|
735
|
-
raise PlugwiseError(
|
736
|
-
"Plugwise: failed setting temperature: no valid input provided"
|
737
|
-
)
|
738
|
-
tmp_setpoint_high = items["setpoint_high"]
|
739
|
-
tmp_setpoint_low = items["setpoint_low"]
|
740
|
-
if self._cooling_enabled: # in cooling mode
|
741
|
-
setpoint = tmp_setpoint_high
|
742
|
-
if tmp_setpoint_low != MIN_SETPOINT:
|
743
|
-
raise PlugwiseError(
|
744
|
-
"Plugwise: heating setpoint cannot be changed when in cooling mode"
|
745
|
-
)
|
746
|
-
else: # in heating mode
|
747
|
-
setpoint = tmp_setpoint_low
|
748
|
-
if tmp_setpoint_high != MAX_SETPOINT:
|
749
|
-
raise PlugwiseError(
|
750
|
-
"Plugwise: cooling setpoint cannot be changed when in heating mode"
|
751
|
-
)
|
752
|
-
|
753
|
-
if setpoint is None:
|
336
|
+
try:
|
337
|
+
await self._smile_api.set_temperature(loc_id, items)
|
338
|
+
except PlugwiseError as exc:
|
754
339
|
raise PlugwiseError(
|
755
|
-
|
756
|
-
|
757
|
-
|
758
|
-
temperature = str(setpoint)
|
759
|
-
uri = self._thermostat_uri(loc_id)
|
760
|
-
data = (
|
761
|
-
"<thermostat_functionality><setpoint>"
|
762
|
-
f"{temperature}</setpoint></thermostat_functionality>"
|
763
|
-
)
|
764
|
-
|
765
|
-
await self._request(uri, method="put", data=data)
|
340
|
+
"Plugwise: failed setting temperature: no valid input provided"
|
341
|
+
) from exc
|
766
342
|
|
767
343
|
async def set_number_setpoint(self, key: str, _: str, temperature: float) -> None:
|
768
344
|
"""Set the max. Boiler or DHW setpoint on the Central Heating boiler."""
|
769
|
-
|
770
|
-
|
771
|
-
|
772
|
-
|
773
|
-
for th_func in th_func_list:
|
774
|
-
if th_func.find("type").text == key:
|
775
|
-
thermostat_id = th_func.attrib["id"]
|
776
|
-
|
777
|
-
if thermostat_id is None:
|
778
|
-
raise PlugwiseError(f"Plugwise: cannot change setpoint, {key} not found.")
|
779
|
-
|
780
|
-
uri = f"{APPLIANCES};id={self._heater_id}/thermostat;id={thermostat_id}"
|
781
|
-
data = f"<thermostat_functionality><setpoint>{temp}</setpoint></thermostat_functionality>"
|
782
|
-
await self._request(uri, method="put", data=data)
|
345
|
+
try:
|
346
|
+
await self._smile_api.set_number_setpoint(key, temperature)
|
347
|
+
except PlugwiseError as exc:
|
348
|
+
raise PlugwiseError(f"Plugwise: cannot change setpoint, {key} not found.") from exc
|
783
349
|
|
784
350
|
async def set_temperature_offset(self, _: str, dev_id: str, offset: float) -> None:
|
785
351
|
"""Set the Temperature offset for thermostats that support this feature."""
|
786
|
-
|
352
|
+
try:
|
353
|
+
await self._smile_api.set_temperature_offset(dev_id, offset)
|
354
|
+
except PlugwiseError as exc:
|
787
355
|
raise PlugwiseError(
|
788
356
|
"Plugwise: this device does not have temperature-offset capability."
|
789
|
-
)
|
790
|
-
|
791
|
-
value = str(offset)
|
792
|
-
uri = f"{APPLIANCES};id={dev_id}/offset;type=temperature_offset"
|
793
|
-
data = f"<offset_functionality><offset>{value}</offset></offset_functionality>"
|
794
|
-
|
795
|
-
await self._request(uri, method="put", data=data)
|
796
|
-
|
797
|
-
async def _set_groupswitch_member_state(
|
798
|
-
self, members: list[str], state: str, switch: Munch
|
799
|
-
) -> None:
|
800
|
-
"""Helper-function for set_switch_state().
|
801
|
-
|
802
|
-
Set the given State of the relevant Switch within a group of members.
|
803
|
-
"""
|
804
|
-
for member in members:
|
805
|
-
locator = f'appliance[@id="{member}"]/{switch.actuator}/{switch.func_type}'
|
806
|
-
switch_id = self._appliances.find(locator).attrib["id"]
|
807
|
-
uri = f"{APPLIANCES};id={member}/{switch.device};id={switch_id}"
|
808
|
-
if self._stretch_v2:
|
809
|
-
uri = f"{APPLIANCES};id={member}/{switch.device}"
|
810
|
-
data = f"<{switch.func_type}><{switch.func}>{state}</{switch.func}></{switch.func_type}>"
|
811
|
-
|
812
|
-
await self._request(uri, method="put", data=data)
|
357
|
+
) from exc
|
813
358
|
|
814
359
|
async def set_switch_state(
|
815
360
|
self, appl_id: str, members: list[str] | None, model: str, state: str
|
816
361
|
) -> None:
|
817
362
|
"""Set the given State of the relevant Switch."""
|
818
|
-
|
819
|
-
|
820
|
-
|
821
|
-
|
822
|
-
switch.func = "state"
|
823
|
-
if model == "dhw_cm_switch":
|
824
|
-
switch.device = "toggle"
|
825
|
-
switch.func_type = "toggle_functionality"
|
826
|
-
switch.act_type = "domestic_hot_water_comfort_mode"
|
827
|
-
|
828
|
-
if model == "cooling_ena_switch":
|
829
|
-
switch.device = "toggle"
|
830
|
-
switch.func_type = "toggle_functionality"
|
831
|
-
switch.act_type = "cooling_enabled"
|
832
|
-
|
833
|
-
if model == "lock":
|
834
|
-
switch.func = "lock"
|
835
|
-
state = "false" if state == "off" else "true"
|
836
|
-
|
837
|
-
if self._stretch_v2:
|
838
|
-
switch.actuator = "actuators"
|
839
|
-
switch.func_type = "relay"
|
840
|
-
|
841
|
-
if members is not None:
|
842
|
-
return await self._set_groupswitch_member_state(members, state, switch)
|
843
|
-
|
844
|
-
locator = f'appliance[@id="{appl_id}"]/{switch.actuator}/{switch.func_type}'
|
845
|
-
found: list[etree] = self._appliances.findall(locator)
|
846
|
-
for item in found:
|
847
|
-
if (sw_type := item.find("type")) is not None:
|
848
|
-
if sw_type.text == switch.act_type:
|
849
|
-
switch_id = item.attrib["id"]
|
850
|
-
else:
|
851
|
-
switch_id = item.attrib["id"]
|
852
|
-
break
|
853
|
-
|
854
|
-
uri = f"{APPLIANCES};id={appl_id}/{switch.device};id={switch_id}"
|
855
|
-
if self._stretch_v2:
|
856
|
-
uri = f"{APPLIANCES};id={appl_id}/{switch.device}"
|
857
|
-
data = f"<{switch.func_type}><{switch.func}>{state}</{switch.func}></{switch.func_type}>"
|
858
|
-
|
859
|
-
if model == "relay":
|
860
|
-
locator = (
|
861
|
-
f'appliance[@id="{appl_id}"]/{switch.actuator}/{switch.func_type}/lock'
|
862
|
-
)
|
863
|
-
# Don't bother switching a relay when the corresponding lock-state is true
|
864
|
-
if self._appliances.find(locator).text == "true":
|
865
|
-
raise PlugwiseError("Plugwise: the locked Relay was not switched.")
|
866
|
-
|
867
|
-
await self._request(uri, method="put", data=data)
|
363
|
+
try:
|
364
|
+
await self._smile_api.set_switch_state(appl_id, members, model, state)
|
365
|
+
except PlugwiseError as exc:
|
366
|
+
raise PlugwiseError("Plugwise: the locked Relay was not switched.") from exc
|
868
367
|
|
869
368
|
async def set_gateway_mode(self, mode: str) -> None:
|
870
369
|
"""Set the gateway mode."""
|
871
|
-
|
872
|
-
|
873
|
-
|
874
|
-
|
875
|
-
away_time = time_1.isoformat(timespec="milliseconds") + "Z"
|
876
|
-
time_2 = str(dt.date.today() - dt.timedelta(1))
|
877
|
-
vacation_time = time_2 + "T23:00:00.000Z"
|
878
|
-
end_time = "2037-04-21T08:00:53.000Z"
|
879
|
-
valid = ""
|
880
|
-
if mode == "away":
|
881
|
-
valid = (
|
882
|
-
f"<valid_from>{away_time}</valid_from><valid_to>{end_time}</valid_to>"
|
883
|
-
)
|
884
|
-
if mode == "vacation":
|
885
|
-
valid = f"<valid_from>{vacation_time}</valid_from><valid_to>{end_time}</valid_to>"
|
886
|
-
|
887
|
-
uri = f"{APPLIANCES};type=gateway/gateway_mode_control"
|
888
|
-
data = f"<gateway_mode_control_functionality><mode>{mode}</mode>{valid}</gateway_mode_control_functionality>"
|
889
|
-
|
890
|
-
await self._request(uri, method="put", data=data)
|
370
|
+
try:
|
371
|
+
await self._smile_api.set_gateway_mode(mode)
|
372
|
+
except PlugwiseError as exc:
|
373
|
+
raise PlugwiseError("Plugwise: invalid gateway mode.") from exc
|
891
374
|
|
892
375
|
async def set_regulation_mode(self, mode: str) -> None:
|
893
376
|
"""Set the heating regulation mode."""
|
894
|
-
|
895
|
-
|
896
|
-
|
897
|
-
|
898
|
-
duration = ""
|
899
|
-
if "bleeding" in mode:
|
900
|
-
duration = "<duration>300</duration>"
|
901
|
-
data = f"<regulation_mode_control_functionality>{duration}<mode>{mode}</mode></regulation_mode_control_functionality>"
|
902
|
-
|
903
|
-
await self._request(uri, method="put", data=data)
|
377
|
+
try:
|
378
|
+
await self._smile_api.set_regulation_mode(mode)
|
379
|
+
except PlugwiseError as exc:
|
380
|
+
raise PlugwiseError("Plugwise: invalid regulation mode.") from exc
|
904
381
|
|
905
382
|
async def set_dhw_mode(self, mode: str) -> None:
|
906
383
|
"""Set the domestic hot water heating regulation mode."""
|
907
|
-
|
908
|
-
|
909
|
-
|
910
|
-
|
911
|
-
data = f"<domestic_hot_water_mode_control_functionality><mode>{mode}</mode></domestic_hot_water_mode_control_functionality>"
|
912
|
-
|
913
|
-
await self._request(uri, method="put", data=data)
|
384
|
+
try:
|
385
|
+
await self._smile_api.set_dhw_mode(mode)
|
386
|
+
except PlugwiseError as exc:
|
387
|
+
raise PlugwiseError("Plugwise: invalid dhw mode.") from exc
|
914
388
|
|
915
389
|
async def delete_notification(self) -> None:
|
916
390
|
"""Delete the active Plugwise Notification."""
|
917
|
-
await self.
|
391
|
+
await self._smile_api.delete_notification()
|