plugwise 0.35.0__tar.gz → 0.35.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {plugwise-0.35.0 → plugwise-0.35.2}/PKG-INFO +1 -1
- {plugwise-0.35.0 → plugwise-0.35.2}/plugwise/__init__.py +141 -176
- {plugwise-0.35.0 → plugwise-0.35.2}/plugwise/constants.py +2 -18
- {plugwise-0.35.0 → plugwise-0.35.2}/plugwise/helper.py +51 -17
- {plugwise-0.35.0 → plugwise-0.35.2}/plugwise.egg-info/PKG-INFO +1 -1
- {plugwise-0.35.0 → plugwise-0.35.2}/pyproject.toml +1 -1
- {plugwise-0.35.0 → plugwise-0.35.2}/LICENSE +0 -0
- {plugwise-0.35.0 → plugwise-0.35.2}/README.md +0 -0
- {plugwise-0.35.0 → plugwise-0.35.2}/plugwise/exceptions.py +0 -0
- {plugwise-0.35.0 → plugwise-0.35.2}/plugwise/py.typed +0 -0
- {plugwise-0.35.0 → plugwise-0.35.2}/plugwise/util.py +0 -0
- {plugwise-0.35.0 → plugwise-0.35.2}/plugwise.egg-info/SOURCES.txt +0 -0
- {plugwise-0.35.0 → plugwise-0.35.2}/plugwise.egg-info/dependency_links.txt +0 -0
- {plugwise-0.35.0 → plugwise-0.35.2}/plugwise.egg-info/requires.txt +0 -0
- {plugwise-0.35.0 → plugwise-0.35.2}/plugwise.egg-info/top_level.txt +0 -0
- {plugwise-0.35.0 → plugwise-0.35.2}/setup.cfg +0 -0
- {plugwise-0.35.0 → plugwise-0.35.2}/setup.py +0 -0
@@ -17,6 +17,7 @@ import semver
|
|
17
17
|
|
18
18
|
from .constants import (
|
19
19
|
ADAM,
|
20
|
+
ANNA,
|
20
21
|
APPLIANCES,
|
21
22
|
DEFAULT_PORT,
|
22
23
|
DEFAULT_TIMEOUT,
|
@@ -51,7 +52,7 @@ from .exceptions import (
|
|
51
52
|
from .helper import SmileComm, SmileHelper
|
52
53
|
|
53
54
|
|
54
|
-
def remove_empty_platform_dicts(data: DeviceData) ->
|
55
|
+
def remove_empty_platform_dicts(data: DeviceData) -> None:
|
55
56
|
"""Helper-function for removing any empty platform dicts."""
|
56
57
|
if not data["binary_sensors"]:
|
57
58
|
data.pop("binary_sensors")
|
@@ -60,16 +61,47 @@ def remove_empty_platform_dicts(data: DeviceData) -> DeviceData:
|
|
60
61
|
if not data["switches"]:
|
61
62
|
data.pop("switches")
|
62
63
|
|
63
|
-
return data
|
64
|
-
|
65
64
|
|
66
65
|
class SmileData(SmileHelper):
|
67
66
|
"""The Plugwise Smile main class."""
|
68
67
|
|
69
|
-
def
|
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:
|
70
98
|
"""Helper-function for adding/updating various cooling-related values."""
|
71
|
-
# For heating + cooling, replace setpoint with setpoint_high/_low
|
72
|
-
if
|
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
|
+
):
|
73
105
|
thermostat = device["thermostat"]
|
74
106
|
sensors = device["sensors"]
|
75
107
|
temp_dict: ActuatorData = {
|
@@ -90,36 +122,27 @@ class SmileData(SmileHelper):
|
|
90
122
|
sensors["setpoint_high"] = temp_dict["setpoint_high"]
|
91
123
|
self._count += 2
|
92
124
|
|
93
|
-
|
94
|
-
|
95
|
-
def _update_gw_devices(self) -> None:
|
96
|
-
"""Helper-function for _all_device_data() and async_update().
|
125
|
+
def get_all_devices(self) -> None:
|
126
|
+
"""Determine the evices present from the obtained XML-data.
|
97
127
|
|
98
|
-
|
128
|
+
Run this functions once to gather the initial device configuration,
|
129
|
+
then regularly run async_update() to refresh the device data.
|
99
130
|
"""
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
self._is_thermostat
|
109
|
-
or (self.smile_type == "power" and not self._smile_legacy)
|
110
|
-
)
|
111
|
-
):
|
112
|
-
data["binary_sensors"]["plugwise_notification"] = bool(
|
113
|
-
self._notifications
|
114
|
-
)
|
115
|
-
self._count += 1
|
116
|
-
device.update(data)
|
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
|
+
)
|
117
139
|
|
118
|
-
|
119
|
-
|
120
|
-
|
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)
|
121
143
|
|
122
|
-
|
144
|
+
# Collect the remaining data for all devices
|
145
|
+
self._all_device_data()
|
123
146
|
|
124
147
|
def _all_device_data(self) -> None:
|
125
148
|
"""Helper-function for get_all_devices().
|
@@ -145,67 +168,49 @@ class SmileData(SmileHelper):
|
|
145
168
|
{"heater_id": self._heater_id, "cooling_present": self._cooling_present}
|
146
169
|
)
|
147
170
|
|
148
|
-
def get_all_devices(self) -> None:
|
149
|
-
"""Determine the evices present from the obtained XML-data.
|
150
|
-
|
151
|
-
Run this functions once to gather the initial device configuration,
|
152
|
-
then regularly run async_update() to refresh the device data.
|
153
|
-
"""
|
154
|
-
# Gather all the devices and their initial data
|
155
|
-
self._all_appliances()
|
156
|
-
if self.smile_type == "thermostat":
|
157
|
-
self._scan_thermostats()
|
158
|
-
# Collect a list of thermostats with offset-capability
|
159
|
-
self.therms_with_offset_func = (
|
160
|
-
self._get_appliances_with_offset_functionality()
|
161
|
-
)
|
162
|
-
|
163
|
-
# Collect switching- or pump-group data
|
164
|
-
if group_data := self._get_group_switches():
|
165
|
-
self.gw_devices.update(group_data)
|
166
|
-
|
167
|
-
# Collect the remaining data for all device
|
168
|
-
self._all_device_data()
|
169
|
-
|
170
171
|
def _device_data_switching_group(
|
171
|
-
self, device: DeviceData,
|
172
|
-
) ->
|
172
|
+
self, device: DeviceData, data: DeviceData
|
173
|
+
) -> None:
|
173
174
|
"""Helper-function for _get_device_data().
|
174
175
|
|
175
176
|
Determine switching group device data.
|
176
177
|
"""
|
177
|
-
if device["dev_class"]
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
device_data["switches"]["relay"] = counter != 0
|
185
|
-
self._count += 1
|
186
|
-
return device_data
|
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
|
187
185
|
|
188
|
-
def _device_data_adam(
|
189
|
-
self, device: DeviceData, device_data: DeviceData
|
190
|
-
) -> DeviceData:
|
186
|
+
def _device_data_adam(self, device: DeviceData, data: DeviceData) -> None:
|
191
187
|
"""Helper-function for _get_device_data().
|
192
188
|
|
193
|
-
Determine Adam heating-status for on-off heating via valves
|
189
|
+
Determine Adam heating-status for on-off heating via valves,
|
190
|
+
available regulations_modes and thermostat control_states.
|
194
191
|
"""
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
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 for Adam
|
202
|
+
if device["dev_class"] == "gateway" and self._reg_allowed_modes:
|
203
|
+
data["regulation_modes"] = self._reg_allowed_modes
|
204
|
+
self._count += 1
|
203
205
|
|
204
|
-
|
206
|
+
# Control_state, only for Adam master thermostats
|
207
|
+
if device["dev_class"] in ZONE_THERMOSTATS:
|
208
|
+
loc_id = device["location"]
|
209
|
+
if ctrl_state := self._control_state(loc_id):
|
210
|
+
data["control_state"] = ctrl_state
|
211
|
+
self._count += 1
|
205
212
|
|
206
|
-
def _device_data_climate(
|
207
|
-
self, device: DeviceData, device_data: DeviceData
|
208
|
-
) -> DeviceData:
|
213
|
+
def _device_data_climate(self, device: DeviceData, data: DeviceData) -> None:
|
209
214
|
"""Helper-function for _get_device_data().
|
210
215
|
|
211
216
|
Determine climate-control device data.
|
@@ -213,44 +218,34 @@ class SmileData(SmileHelper):
|
|
213
218
|
loc_id = device["location"]
|
214
219
|
|
215
220
|
# Presets
|
216
|
-
|
217
|
-
|
221
|
+
data["preset_modes"] = None
|
222
|
+
data["active_preset"] = None
|
218
223
|
self._count += 2
|
219
224
|
if presets := self._presets(loc_id):
|
220
|
-
|
221
|
-
|
225
|
+
data["preset_modes"] = list(presets)
|
226
|
+
data["active_preset"] = self._preset(loc_id)
|
222
227
|
|
223
228
|
# Schedule
|
224
229
|
avail_schedules, sel_schedule = self._schedules(loc_id)
|
225
|
-
|
226
|
-
|
230
|
+
data["available_schedules"] = avail_schedules
|
231
|
+
data["select_schedule"] = sel_schedule
|
227
232
|
self._count += 2
|
228
233
|
|
229
|
-
# Control_state, only for Adam master thermostats
|
230
|
-
if ctrl_state := self._control_state(loc_id):
|
231
|
-
device_data["control_state"] = ctrl_state
|
232
|
-
self._count += 1
|
233
|
-
|
234
234
|
# Operation modes: auto, heat, heat_cool, cool and off
|
235
|
-
|
235
|
+
data["mode"] = "auto"
|
236
236
|
self._count += 1
|
237
237
|
if sel_schedule == NONE:
|
238
|
-
|
238
|
+
data["mode"] = "heat"
|
239
239
|
if self._cooling_present:
|
240
|
-
|
241
|
-
"cool" if self.check_reg_mode("cooling") else "heat_cool"
|
242
|
-
)
|
240
|
+
data["mode"] = "cool" if self.check_reg_mode("cooling") else "heat_cool"
|
243
241
|
|
244
242
|
if self.check_reg_mode("off"):
|
245
|
-
|
243
|
+
data["mode"] = "off"
|
246
244
|
|
247
|
-
if NONE in avail_schedules:
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
loc_id, avail_schedules, sel_schedule, device_data
|
252
|
-
)
|
253
|
-
return device_data
|
245
|
+
if NONE not in avail_schedules:
|
246
|
+
self._get_schedule_states_with_off(
|
247
|
+
loc_id, avail_schedules, sel_schedule, data
|
248
|
+
)
|
254
249
|
|
255
250
|
def check_reg_mode(self, mode: str) -> bool:
|
256
251
|
"""Helper-function for device_data_climate()."""
|
@@ -261,7 +256,7 @@ class SmileData(SmileHelper):
|
|
261
256
|
|
262
257
|
def _get_schedule_states_with_off(
|
263
258
|
self, location: str, schedules: list[str], selected: str, data: DeviceData
|
264
|
-
) ->
|
259
|
+
) -> None:
|
265
260
|
"""Collect schedules with states for each thermostat.
|
266
261
|
|
267
262
|
Also, replace NONE by OFF when none of the schedules are active,
|
@@ -282,34 +277,20 @@ class SmileData(SmileHelper):
|
|
282
277
|
if all_off:
|
283
278
|
data["select_schedule"] = OFF
|
284
279
|
|
285
|
-
return data
|
286
|
-
|
287
280
|
def _check_availability(
|
288
|
-
self, device: DeviceData,
|
289
|
-
) ->
|
281
|
+
self, device: DeviceData, dev_class: str, data: DeviceData, message: str
|
282
|
+
) -> None:
|
290
283
|
"""Helper-function for _get_device_data().
|
291
284
|
|
292
285
|
Provide availability status for the wired-commected devices.
|
293
286
|
"""
|
294
|
-
|
295
|
-
|
296
|
-
device_data["available"] = True
|
287
|
+
if device["dev_class"] == dev_class:
|
288
|
+
data["available"] = True
|
297
289
|
self._count += 1
|
298
|
-
for
|
299
|
-
for msg in
|
300
|
-
if
|
301
|
-
|
302
|
-
|
303
|
-
# Smartmeter
|
304
|
-
if device["dev_class"] == "smartmeter":
|
305
|
-
device_data["available"] = True
|
306
|
-
self._count += 1
|
307
|
-
for data in self._notifications.values():
|
308
|
-
for msg in data.values():
|
309
|
-
if "P1 does not seem to be connected to a smart meter" in msg:
|
310
|
-
device_data["available"] = False
|
311
|
-
|
312
|
-
return device_data
|
290
|
+
for item in self._notifications.values():
|
291
|
+
for msg in item.values():
|
292
|
+
if message in msg:
|
293
|
+
data["available"] = False
|
313
294
|
|
314
295
|
def _get_device_data(self, dev_id: str) -> DeviceData:
|
315
296
|
"""Helper-function for _all_device_data() and async_update().
|
@@ -317,44 +298,32 @@ class SmileData(SmileHelper):
|
|
317
298
|
Provide device-data, based on Location ID (= dev_id), from APPLIANCES.
|
318
299
|
"""
|
319
300
|
device = self.gw_devices[dev_id]
|
320
|
-
|
321
|
-
# Generic
|
322
|
-
if self.smile_type == "thermostat" and device["dev_class"] == "gateway":
|
323
|
-
# Adam & Anna: the Smile outdoor_temperature is present in DOMAIN_OBJECTS and LOCATIONS - under Home
|
324
|
-
# The outdoor_temperature present in APPLIANCES is a local sensor connected to the active device
|
325
|
-
outdoor_temperature = self._object_value(
|
326
|
-
self._home_location, "outdoor_temperature"
|
327
|
-
)
|
328
|
-
if outdoor_temperature is not None:
|
329
|
-
device_data["sensors"]["outdoor_temperature"] = outdoor_temperature
|
330
|
-
self._count += 1
|
331
|
-
|
332
|
-
# Show the allowed regulation modes
|
333
|
-
if self._reg_allowed_modes:
|
334
|
-
device_data["regulation_modes"] = self._reg_allowed_modes
|
335
|
-
self._count += 1
|
336
|
-
|
337
|
-
# Show the allowed dhw_modes
|
338
|
-
if device["dev_class"] == "heater_central" and self._dhw_allowed_modes:
|
339
|
-
device_data["dhw_modes"] = self._dhw_allowed_modes
|
340
|
-
self._count += 1
|
301
|
+
data = self._get_measurement_data(dev_id)
|
341
302
|
|
342
303
|
# Check availability of non-legacy wired-connected devices
|
343
304
|
if not self._smile_legacy:
|
344
|
-
|
305
|
+
# Smartmeter
|
306
|
+
self._check_availability(
|
307
|
+
device, "smartmeter", data, "P1 does not seem to be connected"
|
308
|
+
)
|
309
|
+
# OpenTherm device
|
310
|
+
if device["name"] != "OnOff":
|
311
|
+
self._check_availability(
|
312
|
+
device, "heater_central", data, "no OpenTherm communication"
|
313
|
+
)
|
345
314
|
|
346
315
|
# Switching groups data
|
347
|
-
|
348
|
-
#
|
349
|
-
|
350
|
-
#
|
316
|
+
self._device_data_switching_group(device, data)
|
317
|
+
# Adam data
|
318
|
+
self._device_data_adam(device, data)
|
319
|
+
# Skip obtaining data for non master-thermostats
|
351
320
|
if device["dev_class"] not in ZONE_THERMOSTATS:
|
352
|
-
return
|
321
|
+
return data
|
353
322
|
|
354
323
|
# Thermostat data (presets, temperatures etc)
|
355
|
-
|
324
|
+
self._device_data_climate(device, data)
|
356
325
|
|
357
|
-
return
|
326
|
+
return data
|
358
327
|
|
359
328
|
|
360
329
|
class Smile(SmileComm, SmileData):
|
@@ -539,14 +508,18 @@ class Smile(SmileComm, SmileData):
|
|
539
508
|
if result.find(locator_2) is not None:
|
540
509
|
self._elga = True
|
541
510
|
|
542
|
-
async def
|
543
|
-
"""
|
544
|
-
|
545
|
-
Request domain_objects data.
|
546
|
-
"""
|
511
|
+
async def _full_update_device(self) -> None:
|
512
|
+
"""Perform a first fetch of all XML data, needed for initialization."""
|
547
513
|
self._domain_objects = await self._request(DOMAIN_OBJECTS)
|
514
|
+
self._get_plugwise_notifications()
|
515
|
+
self._locations = await self._request(LOCATIONS)
|
516
|
+
self._modules = await self._request(MODULES)
|
517
|
+
# P1 legacy has no appliances
|
518
|
+
if not (self.smile_type == "power" and self._smile_legacy):
|
519
|
+
self._appliances = await self._request(APPLIANCES)
|
548
520
|
|
549
|
-
|
521
|
+
def _get_plugwise_notifications(self) -> None:
|
522
|
+
"""Collect the Plugwise notifications."""
|
550
523
|
self._notifications = {}
|
551
524
|
for notification in self._domain_objects.findall("./notification"):
|
552
525
|
try:
|
@@ -561,15 +534,6 @@ class Smile(SmileComm, SmileData):
|
|
561
534
|
f"{self._endpoint}{DOMAIN_OBJECTS}",
|
562
535
|
)
|
563
536
|
|
564
|
-
async def _full_update_device(self) -> None:
|
565
|
-
"""Perform a first fetch of all XML data, needed for initialization."""
|
566
|
-
await self._update_domain_objects()
|
567
|
-
self._locations = await self._request(LOCATIONS)
|
568
|
-
self._modules = await self._request(MODULES)
|
569
|
-
# P1 legacy has no appliances
|
570
|
-
if not (self.smile_type == "power" and self._smile_legacy):
|
571
|
-
self._appliances = await self._request(APPLIANCES)
|
572
|
-
|
573
537
|
async def async_update(self) -> PlugwiseData:
|
574
538
|
"""Perform an incremental update for updating the various device states."""
|
575
539
|
# Perform a full update at day-change
|
@@ -587,7 +551,8 @@ class Smile(SmileComm, SmileData):
|
|
587
551
|
self.get_all_devices()
|
588
552
|
# Otherwise perform an incremental update
|
589
553
|
else:
|
590
|
-
await self.
|
554
|
+
self._domain_objects = await self._request(DOMAIN_OBJECTS)
|
555
|
+
self._get_plugwise_notifications()
|
591
556
|
match self._target_smile:
|
592
557
|
case "smile_v2":
|
593
558
|
self._modules = await self._request(MODULES)
|
@@ -701,7 +666,7 @@ class Smile(SmileComm, SmileData):
|
|
701
666
|
template = (
|
702
667
|
'<template tag="zone_preset_based_on_time_and_presence_with_override" />'
|
703
668
|
)
|
704
|
-
if
|
669
|
+
if self.smile(ANNA):
|
705
670
|
locator = f'.//*[@id="{schedule_rule_id}"]/template'
|
706
671
|
template_id = self._domain_objects.find(locator).attrib["id"]
|
707
672
|
template = f'<template id="{template_id}" />'
|
@@ -755,7 +720,7 @@ class Smile(SmileComm, SmileData):
|
|
755
720
|
if "setpoint" in items:
|
756
721
|
setpoint = items["setpoint"]
|
757
722
|
|
758
|
-
if self.
|
723
|
+
if self.smile(ANNA) and self._cooling_present:
|
759
724
|
if "setpoint_high" not in items:
|
760
725
|
raise PlugwiseError(
|
761
726
|
"Plugwise: failed setting temperature: no valid input provided"
|
@@ -80,7 +80,7 @@ HW_MODELS: Final[dict[str, str]] = {
|
|
80
80
|
MAX_SETPOINT: Final[float] = 30.0
|
81
81
|
MIN_SETPOINT: Final[float] = 4.0
|
82
82
|
NONE: Final = "None"
|
83
|
-
OFF: Final = "
|
83
|
+
OFF: Final = "off"
|
84
84
|
|
85
85
|
# XML data paths
|
86
86
|
APPLIANCES: Final = "/core/appliances"
|
@@ -257,12 +257,6 @@ BinarySensorType = Literal[
|
|
257
257
|
]
|
258
258
|
BINARY_SENSORS: Final[tuple[str, ...]] = get_args(BinarySensorType)
|
259
259
|
|
260
|
-
NumberType = Literal[
|
261
|
-
"maximum_boiler_temperature",
|
262
|
-
"max_dhw_temperature",
|
263
|
-
"temperature_offset",
|
264
|
-
]
|
265
|
-
|
266
260
|
LIMITS: Final[tuple[str, ...]] = (
|
267
261
|
"offset",
|
268
262
|
"setpoint",
|
@@ -271,17 +265,6 @@ LIMITS: Final[tuple[str, ...]] = (
|
|
271
265
|
"upper_bound",
|
272
266
|
)
|
273
267
|
|
274
|
-
SelectType = Literal[
|
275
|
-
"select_dhw_mode",
|
276
|
-
"select_regulation_mode",
|
277
|
-
"select_schedule",
|
278
|
-
]
|
279
|
-
SelectOptionsType = Literal[
|
280
|
-
"dhw_modes",
|
281
|
-
"regulation_modes",
|
282
|
-
"available_schedules",
|
283
|
-
]
|
284
|
-
|
285
268
|
SensorType = Literal[
|
286
269
|
"battery",
|
287
270
|
"cooling_activation_outdoor_temperature",
|
@@ -505,6 +488,7 @@ class DeviceData(TypedDict, total=False):
|
|
505
488
|
"""The Device Data class, covering the collected and ordered output-data per device."""
|
506
489
|
|
507
490
|
# Appliance base data
|
491
|
+
has_actuators: bool
|
508
492
|
dev_class: str
|
509
493
|
firmware: str | None
|
510
494
|
hardware: str
|
@@ -570,15 +570,21 @@ class SmileHelper:
|
|
570
570
|
# Legacy P1 has no more devices
|
571
571
|
return
|
572
572
|
|
573
|
+
hc_count = 0
|
573
574
|
for appliance in self._appliances.findall("./appliance"):
|
574
575
|
appl = Munch()
|
575
576
|
appl.pwclass = appliance.find("type").text
|
576
|
-
#
|
577
|
+
# Count amount of heater_central's
|
578
|
+
if appl.pwclass == "heater_central":
|
579
|
+
hc_count += 1
|
580
|
+
# Mark heater_central and thermostat that don't have actuator_functionalities,
|
581
|
+
# could be an orphaned device (Core #81712, #104433)
|
582
|
+
appl.has_actuators = True
|
577
583
|
if (
|
578
|
-
appl.pwclass
|
584
|
+
appl.pwclass in ["heater_central", "thermostat"]
|
579
585
|
and appliance.find("actuator_functionalities/") is None
|
580
586
|
):
|
581
|
-
|
587
|
+
appl.has_actuators = False
|
582
588
|
|
583
589
|
appl.location = None
|
584
590
|
if (appl_loc := appliance.find("location")) is not None:
|
@@ -622,6 +628,7 @@ class SmileHelper:
|
|
622
628
|
for key, value in {
|
623
629
|
"firmware": appl.firmware,
|
624
630
|
"hardware": appl.hardware,
|
631
|
+
"has_actuators": appl.has_actuators,
|
625
632
|
"location": appl.location,
|
626
633
|
"mac_address": appl.mac,
|
627
634
|
"model": appl.model,
|
@@ -634,6 +641,22 @@ class SmileHelper:
|
|
634
641
|
self.gw_devices[appl.dev_id][appl_key] = value
|
635
642
|
self._count += 1
|
636
643
|
|
644
|
+
# Remove thermostat with empty actuator_functionalities (Core #81712), remove heater_central
|
645
|
+
# with empty actuator_functionalities but only when there are more than one (Core #104433).
|
646
|
+
for dev_id, device in dict(self.gw_devices).items():
|
647
|
+
if device["dev_class"] == "thermostat" or (
|
648
|
+
device["dev_class"] == "heater_central" and hc_count > 1
|
649
|
+
):
|
650
|
+
if not self.gw_devices[dev_id]["has_actuators"]:
|
651
|
+
self._count -= len(self.gw_devices[dev_id])
|
652
|
+
self.gw_devices.pop(dev_id)
|
653
|
+
else:
|
654
|
+
self.gw_devices[dev_id].pop("has_actuators")
|
655
|
+
self._count -= 1
|
656
|
+
elif "has_actuators" in self.gw_devices[dev_id]:
|
657
|
+
self.gw_devices[dev_id].pop("has_actuators")
|
658
|
+
self._count -= 1
|
659
|
+
|
637
660
|
# For non-legacy P1 collect the connected SmartMeter info
|
638
661
|
if self.smile_type == "power":
|
639
662
|
self._p1_smartmeter_info_finder(appl)
|
@@ -1016,6 +1039,10 @@ class SmileHelper:
|
|
1016
1039
|
measurements = DEVICE_MEASUREMENTS
|
1017
1040
|
if self._is_thermostat and dev_id == self._heater_id:
|
1018
1041
|
measurements = HEATER_CENTRAL_MEASUREMENTS
|
1042
|
+
# Show the allowed dhw_modes (Loria only)
|
1043
|
+
if self._dhw_allowed_modes:
|
1044
|
+
data["dhw_modes"] = self._dhw_allowed_modes
|
1045
|
+
# Counting of this item is done in _appliance_measurements()
|
1019
1046
|
|
1020
1047
|
if (
|
1021
1048
|
appliance := self._appliances.find(f'./appliance[@id="{dev_id}"]')
|
@@ -1032,8 +1059,18 @@ class SmileHelper:
|
|
1032
1059
|
# Collect availability-status for wireless connected devices to Adam
|
1033
1060
|
self._wireless_availablity(appliance, data)
|
1034
1061
|
|
1035
|
-
|
1036
|
-
|
1062
|
+
if dev_id == self.gateway_id and self.smile(ADAM):
|
1063
|
+
self._get_regulation_mode(appliance, data)
|
1064
|
+
|
1065
|
+
# Adam & Anna: the Smile outdoor_temperature is present in DOMAIN_OBJECTS and LOCATIONS - under Home
|
1066
|
+
# The outdoor_temperature present in APPLIANCES is a local sensor connected to the active device
|
1067
|
+
if self._is_thermostat and dev_id == self.gateway_id:
|
1068
|
+
outdoor_temperature = self._object_value(
|
1069
|
+
self._home_location, "outdoor_temperature"
|
1070
|
+
)
|
1071
|
+
if outdoor_temperature is not None:
|
1072
|
+
data.update({"sensors": {"outdoor_temperature": outdoor_temperature}})
|
1073
|
+
self._count += 1
|
1037
1074
|
|
1038
1075
|
if "c_heating_state" in data:
|
1039
1076
|
self._process_c_heating_state(data)
|
@@ -1507,15 +1544,12 @@ class SmileHelper:
|
|
1507
1544
|
Obtain the toggle state of a 'toggle' = switch.
|
1508
1545
|
"""
|
1509
1546
|
if xml.find("type").text == "heater_central":
|
1510
|
-
locator = "./actuator_functionalities/toggle_functionality"
|
1511
|
-
if
|
1512
|
-
|
1513
|
-
|
1514
|
-
|
1515
|
-
|
1516
|
-
|
1517
|
-
|
1518
|
-
|
1519
|
-
if toggle == "cooling_enabled" and not self._elga:
|
1520
|
-
data["binary_sensors"].pop("cooling_enabled")
|
1521
|
-
self._count -= 1
|
1547
|
+
locator = f"./actuator_functionalities/toggle_functionality[type='{toggle}']/state"
|
1548
|
+
if (state := xml.find(locator)) is not None:
|
1549
|
+
data["switches"][name] = state.text == "on"
|
1550
|
+
self._count += 1
|
1551
|
+
# Remove the cooling_enabled binary_sensor when the corresponding switch is present
|
1552
|
+
# Except for Elga
|
1553
|
+
if toggle == "cooling_enabled" and not self._elga:
|
1554
|
+
data["binary_sensors"].pop("cooling_enabled")
|
1555
|
+
self._count -= 1
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|