plugwise 1.6.4__py3-none-any.whl → 1.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plugwise/__init__.py +61 -53
- plugwise/common.py +20 -118
- plugwise/constants.py +3 -12
- plugwise/data.py +16 -17
- plugwise/helper.py +48 -257
- plugwise/legacy/data.py +7 -12
- plugwise/legacy/helper.py +19 -50
- plugwise/legacy/smile.py +30 -45
- plugwise/smile.py +29 -44
- plugwise/smilecomm.py +148 -0
- plugwise/util.py +112 -21
- {plugwise-1.6.4.dist-info → plugwise-1.7.0.dist-info}/METADATA +2 -2
- plugwise-1.7.0.dist-info/RECORD +18 -0
- {plugwise-1.6.4.dist-info → plugwise-1.7.0.dist-info}/WHEEL +1 -1
- plugwise-1.6.4.dist-info/RECORD +0 -17
- {plugwise-1.6.4.dist-info → plugwise-1.7.0.dist-info}/LICENSE +0 -0
- {plugwise-1.6.4.dist-info → plugwise-1.7.0.dist-info}/top_level.txt +0 -0
plugwise/helper.py
CHANGED
@@ -5,7 +5,6 @@ Plugwise Smile protocol helpers.
|
|
5
5
|
|
6
6
|
from __future__ import annotations
|
7
7
|
|
8
|
-
import asyncio
|
9
8
|
import datetime as dt
|
10
9
|
from typing import cast
|
11
10
|
|
@@ -38,29 +37,20 @@ from plugwise.constants import (
|
|
38
37
|
ActuatorData,
|
39
38
|
ActuatorDataType,
|
40
39
|
ActuatorType,
|
41
|
-
GatewayData,
|
42
40
|
GwEntityData,
|
43
41
|
SensorType,
|
44
42
|
ThermoLoc,
|
45
43
|
ToggleNameType,
|
46
44
|
)
|
47
|
-
from plugwise.exceptions import (
|
48
|
-
ConnectionFailedError,
|
49
|
-
InvalidAuthentication,
|
50
|
-
InvalidXMLError,
|
51
|
-
ResponseError,
|
52
|
-
)
|
53
45
|
from plugwise.util import (
|
54
46
|
check_model,
|
47
|
+
collect_power_values,
|
55
48
|
common_match_cases,
|
56
|
-
|
49
|
+
count_data_items,
|
57
50
|
format_measure,
|
58
51
|
skip_obsolete_measurements,
|
59
52
|
)
|
60
53
|
|
61
|
-
# This way of importing aiohttp is because of patch/mocking in testing (aiohttp timeouts)
|
62
|
-
from aiohttp import BasicAuth, ClientError, ClientResponse, ClientSession, ClientTimeout
|
63
|
-
|
64
54
|
# Time related
|
65
55
|
from dateutil import tz
|
66
56
|
from dateutil.parser import parse
|
@@ -78,204 +68,25 @@ def search_actuator_functionalities(appliance: etree, actuator: str) -> etree |
|
|
78
68
|
return None
|
79
69
|
|
80
70
|
|
81
|
-
class SmileComm:
|
82
|
-
"""The SmileComm class."""
|
83
|
-
|
84
|
-
def __init__(
|
85
|
-
self,
|
86
|
-
host: str,
|
87
|
-
password: str,
|
88
|
-
port: int,
|
89
|
-
timeout: int,
|
90
|
-
username: str,
|
91
|
-
websession: ClientSession | None,
|
92
|
-
) -> None:
|
93
|
-
"""Set the constructor for this class."""
|
94
|
-
if not websession:
|
95
|
-
aio_timeout = ClientTimeout(total=timeout)
|
96
|
-
|
97
|
-
async def _create_session() -> ClientSession:
|
98
|
-
return ClientSession(timeout=aio_timeout) # pragma: no cover
|
99
|
-
|
100
|
-
loop = asyncio.get_event_loop()
|
101
|
-
if loop.is_running():
|
102
|
-
self._websession = ClientSession(timeout=aio_timeout)
|
103
|
-
else:
|
104
|
-
self._websession = loop.run_until_complete(
|
105
|
-
_create_session()
|
106
|
-
) # pragma: no cover
|
107
|
-
else:
|
108
|
-
self._websession = websession
|
109
|
-
|
110
|
-
# Quickfix IPv6 formatting, not covering
|
111
|
-
if host.count(":") > 2: # pragma: no cover
|
112
|
-
host = f"[{host}]"
|
113
|
-
|
114
|
-
self._auth = BasicAuth(username, password=password)
|
115
|
-
self._endpoint = f"http://{host}:{str(port)}"
|
116
|
-
|
117
|
-
async def _request(
|
118
|
-
self,
|
119
|
-
command: str,
|
120
|
-
retry: int = 3,
|
121
|
-
method: str = "get",
|
122
|
-
data: str | None = None,
|
123
|
-
headers: dict[str, str] | None = None,
|
124
|
-
) -> etree:
|
125
|
-
"""Get/put/delete data from a give URL."""
|
126
|
-
resp: ClientResponse
|
127
|
-
url = f"{self._endpoint}{command}"
|
128
|
-
use_headers = headers
|
129
|
-
|
130
|
-
try:
|
131
|
-
match method:
|
132
|
-
case "delete":
|
133
|
-
resp = await self._websession.delete(url, auth=self._auth)
|
134
|
-
case "get":
|
135
|
-
# Work-around for Stretchv2, should not hurt the other smiles
|
136
|
-
use_headers = {"Accept-Encoding": "gzip"}
|
137
|
-
resp = await self._websession.get(
|
138
|
-
url, headers=use_headers, auth=self._auth
|
139
|
-
)
|
140
|
-
case "post":
|
141
|
-
use_headers = {"Content-type": "text/xml"}
|
142
|
-
resp = await self._websession.post(
|
143
|
-
url,
|
144
|
-
headers=use_headers,
|
145
|
-
data=data,
|
146
|
-
auth=self._auth,
|
147
|
-
)
|
148
|
-
case "put":
|
149
|
-
use_headers = {"Content-type": "text/xml"}
|
150
|
-
resp = await self._websession.put(
|
151
|
-
url,
|
152
|
-
headers=use_headers,
|
153
|
-
data=data,
|
154
|
-
auth=self._auth,
|
155
|
-
)
|
156
|
-
except (
|
157
|
-
ClientError
|
158
|
-
) as exc: # ClientError is an ancestor class of ServerTimeoutError
|
159
|
-
if retry < 1:
|
160
|
-
LOGGER.warning(
|
161
|
-
"Failed sending %s %s to Plugwise Smile, error: %s",
|
162
|
-
method,
|
163
|
-
command,
|
164
|
-
exc,
|
165
|
-
)
|
166
|
-
raise ConnectionFailedError from exc
|
167
|
-
return await self._request(command, retry - 1)
|
168
|
-
|
169
|
-
if resp.status == 504:
|
170
|
-
if retry < 1:
|
171
|
-
LOGGER.warning(
|
172
|
-
"Failed sending %s %s to Plugwise Smile, error: %s",
|
173
|
-
method,
|
174
|
-
command,
|
175
|
-
"504 Gateway Timeout",
|
176
|
-
)
|
177
|
-
raise ConnectionFailedError
|
178
|
-
return await self._request(command, retry - 1)
|
179
|
-
|
180
|
-
return await self._request_validate(resp, method)
|
181
|
-
|
182
|
-
async def _request_validate(self, resp: ClientResponse, method: str) -> etree:
|
183
|
-
"""Helper-function for _request(): validate the returned data."""
|
184
|
-
match resp.status:
|
185
|
-
case 200:
|
186
|
-
# Cornercases for server not responding with 202
|
187
|
-
if method in ("post", "put"):
|
188
|
-
return
|
189
|
-
case 202:
|
190
|
-
# Command accepted gives empty body with status 202
|
191
|
-
return
|
192
|
-
case 401:
|
193
|
-
msg = (
|
194
|
-
"Invalid Plugwise login, please retry with the correct credentials."
|
195
|
-
)
|
196
|
-
LOGGER.error("%s", msg)
|
197
|
-
raise InvalidAuthentication
|
198
|
-
case 405:
|
199
|
-
msg = "405 Method not allowed."
|
200
|
-
LOGGER.error("%s", msg)
|
201
|
-
raise ConnectionFailedError
|
202
|
-
|
203
|
-
if not (result := await resp.text()) or (
|
204
|
-
"<error>" in result and "Not started" not in result
|
205
|
-
):
|
206
|
-
LOGGER.warning("Smile response empty or error in %s", result)
|
207
|
-
raise ResponseError
|
208
|
-
|
209
|
-
try:
|
210
|
-
# Encode to ensure utf8 parsing
|
211
|
-
xml = etree.XML(escape_illegal_xml_characters(result).encode())
|
212
|
-
except etree.ParseError as exc:
|
213
|
-
LOGGER.warning("Smile returns invalid XML for %s", self._endpoint)
|
214
|
-
raise InvalidXMLError from exc
|
215
|
-
|
216
|
-
return xml
|
217
|
-
|
218
|
-
async def close_connection(self) -> None:
|
219
|
-
"""Close the Plugwise connection."""
|
220
|
-
await self._websession.close()
|
221
|
-
|
222
|
-
|
223
71
|
class SmileHelper(SmileCommon):
|
224
72
|
"""The SmileHelper class."""
|
225
73
|
|
226
74
|
def __init__(self) -> None:
|
227
75
|
"""Set the constructor for this class."""
|
228
|
-
self._cooling_present: bool
|
229
|
-
self._count: int
|
230
|
-
self._dhw_allowed_modes: list[str] = []
|
231
|
-
self._domain_objects: etree
|
232
76
|
self._endpoint: str
|
233
77
|
self._elga: bool
|
234
|
-
self._gw_allowed_modes: list[str] = []
|
235
|
-
self._heater_id: str
|
236
|
-
self._home_location: str
|
237
78
|
self._is_thermostat: bool
|
238
79
|
self._last_active: dict[str, str | None]
|
239
|
-
self._last_modified: dict[str, str] = {}
|
240
80
|
self._loc_data: dict[str, ThermoLoc]
|
241
|
-
self._notifications: dict[str, dict[str, str]] = {}
|
242
|
-
self._on_off_device: bool
|
243
|
-
self._opentherm_device: bool
|
244
|
-
self._reg_allowed_modes: list[str] = []
|
245
81
|
self._schedule_old_states: dict[str, dict[str, str]]
|
246
|
-
self.
|
247
|
-
self.
|
248
|
-
self.
|
249
|
-
self._thermo_locs: dict[str, ThermoLoc] = {}
|
250
|
-
###################################################################
|
251
|
-
# '_cooling_enabled' can refer to the state of the Elga heatpump
|
252
|
-
# connected to an Anna. For Elga, 'elga_status_code' in (8, 9)
|
253
|
-
# means cooling mode is available, next to heating mode.
|
254
|
-
# 'elga_status_code' = 8 means cooling is active, 9 means idle.
|
255
|
-
#
|
256
|
-
# '_cooling_enabled' cam refer to the state of the Loria or
|
257
|
-
# Thermastage heatpump connected to an Anna. For these,
|
258
|
-
# 'cooling_enabled' = on means set to cooling mode, instead of to
|
259
|
-
# heating mode.
|
260
|
-
# 'cooling_state' = on means cooling is active.
|
261
|
-
###################################################################
|
262
|
-
self._cooling_active = False
|
263
|
-
self._cooling_enabled = False
|
264
|
-
|
265
|
-
self.gateway_id: str
|
266
|
-
self.gw_data: GatewayData = {}
|
267
|
-
self.gw_entities: dict[str, GwEntityData] = {}
|
268
|
-
self.smile_fw_version: version.Version | None
|
82
|
+
self._gateway_id: str = NONE
|
83
|
+
self._zones: dict[str, GwEntityData]
|
84
|
+
self.gw_entities: dict[str, GwEntityData]
|
269
85
|
self.smile_hw_version: str | None
|
270
86
|
self.smile_mac_address: str | None
|
271
87
|
self.smile_model: str
|
272
88
|
self.smile_model_id: str | None
|
273
|
-
self.smile_name: str
|
274
|
-
self.smile_type: str
|
275
89
|
self.smile_version: version.Version | None
|
276
|
-
self.smile_zigbee_mac_address: str | None
|
277
|
-
self.therms_with_offset_func: list[str] = []
|
278
|
-
self._zones: dict[str, GwEntityData] = {}
|
279
90
|
SmileCommon.__init__(self)
|
280
91
|
|
281
92
|
def _all_appliances(self) -> None:
|
@@ -311,10 +122,10 @@ class SmileHelper(SmileCommon):
|
|
311
122
|
appl.location = None
|
312
123
|
if (appl_loc := appliance.find("location")) is not None:
|
313
124
|
appl.location = appl_loc.attrib["id"]
|
314
|
-
# Don't assign the
|
125
|
+
# Don't assign the _home_loc_id to thermostat-devices without a location,
|
315
126
|
# they are not active
|
316
127
|
elif appl.pwclass not in THERMOSTAT_CLASSES:
|
317
|
-
appl.location = self.
|
128
|
+
appl.location = self._home_loc_id
|
318
129
|
|
319
130
|
# Don't show orphaned thermostat-types
|
320
131
|
if appl.pwclass in THERMOSTAT_CLASSES and appl.location is None:
|
@@ -350,22 +161,16 @@ class SmileHelper(SmileCommon):
|
|
350
161
|
switched to maintain backward compatibility with existing implementations.
|
351
162
|
"""
|
352
163
|
appl = Munch()
|
353
|
-
loc_id = next(iter(self._loc_data.keys()))
|
354
|
-
if (
|
355
|
-
location := self._domain_objects.find(f'./location[@id="{loc_id}"]')
|
356
|
-
) is None:
|
357
|
-
return
|
358
|
-
|
359
164
|
locator = MODULE_LOCATOR
|
360
|
-
module_data = self._get_module_data(
|
165
|
+
module_data = self._get_module_data(self._home_location, locator)
|
361
166
|
if not module_data["contents"]:
|
362
167
|
LOGGER.error("No module data found for SmartMeter") # pragma: no cover
|
363
168
|
return # pragma: no cover
|
364
169
|
appl.available = None
|
365
|
-
appl.entity_id = self.
|
170
|
+
appl.entity_id = self._gateway_id
|
366
171
|
appl.firmware = module_data["firmware_version"]
|
367
172
|
appl.hardware = module_data["hardware_version"]
|
368
|
-
appl.location =
|
173
|
+
appl.location = self._home_loc_id
|
369
174
|
appl.mac = None
|
370
175
|
appl.model = module_data["vendor_model"]
|
371
176
|
appl.model_id = None # don't use model_id for SmartMeter
|
@@ -375,8 +180,8 @@ class SmileHelper(SmileCommon):
|
|
375
180
|
appl.zigbee_mac = None
|
376
181
|
|
377
182
|
# Replace the entity_id of the gateway by the smartmeter location_id
|
378
|
-
self.gw_entities[
|
379
|
-
self.
|
183
|
+
self.gw_entities[self._home_loc_id] = self.gw_entities.pop(self._gateway_id)
|
184
|
+
self._gateway_id = self._home_loc_id
|
380
185
|
|
381
186
|
self._create_gw_entities(appl)
|
382
187
|
|
@@ -398,10 +203,14 @@ class SmileHelper(SmileCommon):
|
|
398
203
|
for location in locations:
|
399
204
|
loc.name = location.find("name").text
|
400
205
|
loc.loc_id = location.attrib["id"]
|
401
|
-
if loc.name == "Home":
|
402
|
-
self._home_location = loc.loc_id
|
403
|
-
|
404
206
|
self._loc_data[loc.loc_id] = {"name": loc.name}
|
207
|
+
if loc.name != "Home":
|
208
|
+
continue
|
209
|
+
|
210
|
+
self._home_loc_id = loc.loc_id
|
211
|
+
self._home_location = self._domain_objects.find(
|
212
|
+
f"./location[@id='{loc.loc_id}']"
|
213
|
+
)
|
405
214
|
|
406
215
|
def _appliance_info_finder(self, appl: Munch, appliance: etree) -> Munch:
|
407
216
|
"""Collect info for all appliances found."""
|
@@ -445,8 +254,8 @@ class SmileHelper(SmileCommon):
|
|
445
254
|
|
446
255
|
def _appl_gateway_info(self, appl: Munch, appliance: etree) -> Munch:
|
447
256
|
"""Helper-function for _appliance_info_finder()."""
|
448
|
-
self.
|
449
|
-
appl.firmware = str(self.
|
257
|
+
self._gateway_id = appliance.attrib["id"]
|
258
|
+
appl.firmware = str(self.smile_version)
|
450
259
|
appl.hardware = self.smile_hw_version
|
451
260
|
appl.mac = self.smile_mac_address
|
452
261
|
appl.model = self.smile_model
|
@@ -485,15 +294,9 @@ class SmileHelper(SmileCommon):
|
|
485
294
|
) is not None and (modes := search.find("allowed_modes")) is not None:
|
486
295
|
for mode in modes:
|
487
296
|
mode_list.append(mode.text)
|
488
|
-
self._check_cooling_mode(mode.text)
|
489
297
|
|
490
298
|
return mode_list
|
491
299
|
|
492
|
-
def _check_cooling_mode(self, mode: str) -> None:
|
493
|
-
"""Check if cooling mode is present and update state."""
|
494
|
-
if mode == "cooling":
|
495
|
-
self._cooling_present = True
|
496
|
-
|
497
300
|
def _get_appliances_with_offset_functionality(self) -> list[str]:
|
498
301
|
"""Helper-function collecting all appliance that have offset_functionality."""
|
499
302
|
therm_list: list[str] = []
|
@@ -532,7 +335,7 @@ class SmileHelper(SmileCommon):
|
|
532
335
|
# !! DON'T CHANGE below two if-lines, will break stuff !!
|
533
336
|
if self.smile_type == "power":
|
534
337
|
if entity["dev_class"] == "smartmeter":
|
535
|
-
data.update(self._power_data_from_location(
|
338
|
+
data.update(self._power_data_from_location())
|
536
339
|
|
537
340
|
return data
|
538
341
|
|
@@ -574,21 +377,20 @@ class SmileHelper(SmileCommon):
|
|
574
377
|
|
575
378
|
return data
|
576
379
|
|
577
|
-
def _power_data_from_location(self
|
380
|
+
def _power_data_from_location(self) -> GwEntityData:
|
578
381
|
"""Helper-function for smile.py: _get_entity_data().
|
579
382
|
|
580
|
-
Collect the power-data
|
383
|
+
Collect the power-data from the Home location.
|
581
384
|
"""
|
582
385
|
data: GwEntityData = {"sensors": {}}
|
583
386
|
loc = Munch()
|
584
387
|
log_list: list[str] = ["point_log", "cumulative_log", "interval_log"]
|
585
388
|
t_string = "tariff"
|
586
389
|
|
587
|
-
|
588
|
-
loc.logs = search.find(f'./location[@id="{loc_id}"]/logs')
|
390
|
+
loc.logs = self._home_location.find("./logs")
|
589
391
|
for loc.measurement, loc.attrs in P1_MEASUREMENTS.items():
|
590
392
|
for loc.log_type in log_list:
|
591
|
-
|
393
|
+
collect_power_values(data, loc, t_string)
|
592
394
|
|
593
395
|
self._count += len(data["sensors"])
|
594
396
|
return data
|
@@ -624,7 +426,7 @@ class SmileHelper(SmileCommon):
|
|
624
426
|
appl_i_loc.text, ENERGY_WATT_HOUR
|
625
427
|
)
|
626
428
|
|
627
|
-
self.
|
429
|
+
self._count = count_data_items(self._count, data)
|
628
430
|
|
629
431
|
def _get_toggle_state(
|
630
432
|
self, xml: etree, toggle: str, name: ToggleNameType, data: GwEntityData
|
@@ -647,7 +449,7 @@ class SmileHelper(SmileCommon):
|
|
647
449
|
msg_id = notification.attrib["id"]
|
648
450
|
msg_type = notification.find("type").text
|
649
451
|
msg = notification.find("message").text
|
650
|
-
self._notifications
|
452
|
+
self._notifications[msg_id] = {msg_type: msg}
|
651
453
|
LOGGER.debug("Plugwise notifications: %s", self._notifications)
|
652
454
|
except AttributeError: # pragma: no cover
|
653
455
|
LOGGER.debug(
|
@@ -724,7 +526,7 @@ class SmileHelper(SmileCommon):
|
|
724
526
|
|
725
527
|
Collect the requested gateway mode.
|
726
528
|
"""
|
727
|
-
if not (self.smile(ADAM) and entity_id == self.
|
529
|
+
if not (self.smile(ADAM) and entity_id == self._gateway_id):
|
728
530
|
return None
|
729
531
|
|
730
532
|
if (search := search_actuator_functionalities(appliance, key)) is not None:
|
@@ -764,31 +566,14 @@ class SmileHelper(SmileCommon):
|
|
764
566
|
self._count += 1
|
765
567
|
|
766
568
|
def _get_gateway_outdoor_temp(self, entity_id: str, data: GwEntityData) -> None:
|
767
|
-
"""Adam & Anna: the Smile outdoor_temperature is present in
|
768
|
-
|
769
|
-
|
770
|
-
|
771
|
-
|
772
|
-
|
773
|
-
self._home_location, "outdoor_temperature"
|
774
|
-
)
|
775
|
-
if outdoor_temperature is not None:
|
776
|
-
data.update({"sensors": {"outdoor_temperature": outdoor_temperature}})
|
569
|
+
"""Adam & Anna: the Smile outdoor_temperature is present in the Home location."""
|
570
|
+
if self._is_thermostat and entity_id == self._gateway_id:
|
571
|
+
locator = "./logs/point_log[type='outdoor_temperature']/period/measurement"
|
572
|
+
if (found := self._home_location.find(locator)) is not None:
|
573
|
+
value = format_measure(found.text, NONE)
|
574
|
+
data.update({"sensors": {"outdoor_temperature": value}})
|
777
575
|
self._count += 1
|
778
576
|
|
779
|
-
def _object_value(self, obj_id: str, measurement: str) -> float | int | None:
|
780
|
-
"""Helper-function for smile.py: _get_entity_data().
|
781
|
-
|
782
|
-
Obtain the value/state for the given object from a location in DOMAIN_OBJECTS
|
783
|
-
"""
|
784
|
-
val: float | int | None = None
|
785
|
-
search = self._domain_objects
|
786
|
-
locator = f'./location[@id="{obj_id}"]/logs/point_log[type="{measurement}"]/period/measurement'
|
787
|
-
if (found := search.find(locator)) is not None:
|
788
|
-
val = format_measure(found.text, NONE)
|
789
|
-
|
790
|
-
return val
|
791
|
-
|
792
577
|
def _process_c_heating_state(self, data: GwEntityData) -> None:
|
793
578
|
"""Helper-function for _get_measurement_data().
|
794
579
|
|
@@ -840,7 +625,11 @@ class SmileHelper(SmileCommon):
|
|
840
625
|
self._update_loria_cooling(data)
|
841
626
|
|
842
627
|
def _update_elga_cooling(self, data: GwEntityData) -> None:
|
843
|
-
"""# Anna+Elga: base cooling_state on the elga-status-code.
|
628
|
+
"""# Anna+Elga: base cooling_state on the elga-status-code.
|
629
|
+
|
630
|
+
For Elga, 'elga_status_code' in (8, 9) means cooling is enabled.
|
631
|
+
'elga_status_code' = 8 means cooling is active, 9 means idle.
|
632
|
+
"""
|
844
633
|
if data["thermostat_supports_cooling"]:
|
845
634
|
# Techneco Elga has cooling-capability
|
846
635
|
self._cooling_present = True
|
@@ -862,7 +651,11 @@ class SmileHelper(SmileCommon):
|
|
862
651
|
self._count -= 1
|
863
652
|
|
864
653
|
def _update_loria_cooling(self, data: GwEntityData) -> None:
|
865
|
-
"""Loria/Thermastage: base cooling-related on cooling_state and modulation_level.
|
654
|
+
"""Loria/Thermastage: base cooling-related on cooling_state and modulation_level.
|
655
|
+
|
656
|
+
For the Loria or Thermastage heatpump connected to an Anna cooling-enabled is
|
657
|
+
indicated via the Cooling Enable switch in the Plugwise App.
|
658
|
+
"""
|
866
659
|
# For Loria/Thermastage it's not clear if cooling_enabled in xml shows the correct status,
|
867
660
|
# setting it specifically:
|
868
661
|
self._cooling_enabled = data["binary_sensors"]["cooling_enabled"] = data[
|
@@ -987,11 +780,9 @@ class SmileHelper(SmileCommon):
|
|
987
780
|
Represents the heating/cooling demand-state of the local primary thermostat.
|
988
781
|
Note: heating or cooling can still be active when the setpoint has been reached.
|
989
782
|
"""
|
990
|
-
locator = f'location[@id="{loc_id}"]'
|
991
|
-
if (
|
992
|
-
|
993
|
-
if (ctrl_state := location.find(locator)) is not None:
|
994
|
-
return str(ctrl_state.text)
|
783
|
+
locator = f'location[@id="{loc_id}"]/actuator_functionalities/thermostat_functionality[type="thermostat"]/control_state'
|
784
|
+
if (ctrl_state := self._domain_objects.find(locator)) is not None:
|
785
|
+
return str(ctrl_state.text)
|
995
786
|
|
996
787
|
# Handle missing control_state in regulation_mode off for firmware >= 3.2.0 (issue #776)
|
997
788
|
# In newer firmware versions, default to "off" when control_state is not present
|
plugwise/legacy/data.py
CHANGED
@@ -7,7 +7,7 @@ from __future__ import annotations
|
|
7
7
|
|
8
8
|
# Dict as class
|
9
9
|
# Version detection
|
10
|
-
from plugwise.constants import NONE, OFF, GwEntityData
|
10
|
+
from plugwise.constants import NONE, OFF, GwEntityData, SmileProps
|
11
11
|
from plugwise.legacy.helper import SmileLegacyHelper
|
12
12
|
from plugwise.util import remove_empty_platform_dicts
|
13
13
|
|
@@ -17,25 +17,20 @@ class SmileLegacyData(SmileLegacyHelper):
|
|
17
17
|
|
18
18
|
def __init__(self) -> None:
|
19
19
|
"""Init."""
|
20
|
+
self._smile_props: SmileProps
|
20
21
|
SmileLegacyHelper.__init__(self)
|
21
22
|
|
22
23
|
def _all_entity_data(self) -> None:
|
23
24
|
"""Helper-function for get_all_gateway_entities().
|
24
25
|
|
25
|
-
Collect data for each entity and add to self.
|
26
|
+
Collect data for each entity and add to self._smile_props and self.gw_entities.
|
26
27
|
"""
|
27
28
|
self._update_gw_entities()
|
28
|
-
self.
|
29
|
-
|
30
|
-
|
31
|
-
"item_count": self._count,
|
32
|
-
"smile_name": self.smile_name,
|
33
|
-
}
|
34
|
-
)
|
29
|
+
self._smile_props["gateway_id"] = self.gateway_id
|
30
|
+
self._smile_props["item_count"] = self._count
|
31
|
+
self._smile_props["smile_name"] = self.smile_name
|
35
32
|
if self._is_thermostat:
|
36
|
-
self.
|
37
|
-
{"heater_id": self._heater_id, "cooling_present": False}
|
38
|
-
)
|
33
|
+
self._smile_props["heater_id"] = self._heater_id
|
39
34
|
|
40
35
|
def _update_gw_entities(self) -> None:
|
41
36
|
"""Helper-function for _all_entity_data() and async_update().
|