plugwise 0.35.1__tar.gz → 0.35.3__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.1 → plugwise-0.35.3}/PKG-INFO +1 -1
- {plugwise-0.35.1 → plugwise-0.35.3}/plugwise/__init__.py +141 -176
- {plugwise-0.35.1 → plugwise-0.35.3}/plugwise/constants.py +0 -17
- {plugwise-0.35.1 → plugwise-0.35.3}/plugwise/helper.py +60 -15
- {plugwise-0.35.1 → plugwise-0.35.3}/plugwise.egg-info/PKG-INFO +1 -1
- {plugwise-0.35.1 → plugwise-0.35.3}/pyproject.toml +1 -1
- {plugwise-0.35.1 → plugwise-0.35.3}/LICENSE +0 -0
- {plugwise-0.35.1 → plugwise-0.35.3}/README.md +0 -0
- {plugwise-0.35.1 → plugwise-0.35.3}/plugwise/exceptions.py +0 -0
- {plugwise-0.35.1 → plugwise-0.35.3}/plugwise/py.typed +0 -0
- {plugwise-0.35.1 → plugwise-0.35.3}/plugwise/util.py +0 -0
- {plugwise-0.35.1 → plugwise-0.35.3}/plugwise.egg-info/SOURCES.txt +0 -0
- {plugwise-0.35.1 → plugwise-0.35.3}/plugwise.egg-info/dependency_links.txt +0 -0
- {plugwise-0.35.1 → plugwise-0.35.3}/plugwise.egg-info/requires.txt +0 -0
- {plugwise-0.35.1 → plugwise-0.35.3}/plugwise.egg-info/top_level.txt +0 -0
- {plugwise-0.35.1 → plugwise-0.35.3}/setup.cfg +0 -0
- {plugwise-0.35.1 → plugwise-0.35.3}/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"
|
@@ -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",
|
@@ -456,7 +456,9 @@ class SmileHelper:
|
|
456
456
|
if not self._opentherm_device and not self._on_off_device:
|
457
457
|
return None
|
458
458
|
|
459
|
-
|
459
|
+
# Find the valid heater_central
|
460
|
+
self._heater_id = self._check_heater_central()
|
461
|
+
|
460
462
|
# Info for On-Off device
|
461
463
|
if self._on_off_device:
|
462
464
|
appl.name = "OnOff"
|
@@ -498,6 +500,34 @@ class SmileHelper:
|
|
498
500
|
|
499
501
|
return appl
|
500
502
|
|
503
|
+
def _check_heater_central(self) -> str:
|
504
|
+
"""Find the valid heater_central, helper-function for _appliance_info_finder().
|
505
|
+
|
506
|
+
Solution for Core Issue #104433,
|
507
|
+
for a system that has two heater_central appliances.
|
508
|
+
"""
|
509
|
+
locator = "./appliance[type='heater_central']"
|
510
|
+
hc_count = 0
|
511
|
+
hc_list: list[dict[str, bool]] = []
|
512
|
+
for heater_central in self._appliances.findall(locator):
|
513
|
+
hc_count += 1
|
514
|
+
hc_id: str = heater_central.attrib["id"]
|
515
|
+
has_actuators: bool = (
|
516
|
+
heater_central.find("actuator_functionalities/") is not None
|
517
|
+
)
|
518
|
+
hc_list.append({hc_id: has_actuators})
|
519
|
+
|
520
|
+
heater_central_id = list(hc_list[0].keys())[0]
|
521
|
+
if hc_count > 1:
|
522
|
+
for item in hc_list:
|
523
|
+
for key, value in item.items():
|
524
|
+
if value:
|
525
|
+
heater_central_id = key
|
526
|
+
# Stop when a valid id is found
|
527
|
+
break
|
528
|
+
|
529
|
+
return heater_central_id
|
530
|
+
|
501
531
|
def _p1_smartmeter_info_finder(self, appl: Munch) -> None:
|
502
532
|
"""Collect P1 DSMR Smartmeter info."""
|
503
533
|
loc_id = next(iter(self._loc_data.keys()))
|
@@ -604,6 +634,10 @@ class SmileHelper:
|
|
604
634
|
if not (appl := self._appliance_info_finder(appliance, appl)):
|
605
635
|
continue
|
606
636
|
|
637
|
+
# Skip orphaned heater_central (Core Issue #104433)
|
638
|
+
if appl.pwclass == "heater_central" and appl.dev_id != self._heater_id:
|
639
|
+
continue
|
640
|
+
|
607
641
|
# P1: for gateway and smartmeter switch device_id - part 1
|
608
642
|
# This is done to avoid breakage in HA Core
|
609
643
|
if appl.pwclass == "gateway" and self.smile_type == "power":
|
@@ -1016,6 +1050,10 @@ class SmileHelper:
|
|
1016
1050
|
measurements = DEVICE_MEASUREMENTS
|
1017
1051
|
if self._is_thermostat and dev_id == self._heater_id:
|
1018
1052
|
measurements = HEATER_CENTRAL_MEASUREMENTS
|
1053
|
+
# Show the allowed dhw_modes (Loria only)
|
1054
|
+
if self._dhw_allowed_modes:
|
1055
|
+
data["dhw_modes"] = self._dhw_allowed_modes
|
1056
|
+
# Counting of this item is done in _appliance_measurements()
|
1019
1057
|
|
1020
1058
|
if (
|
1021
1059
|
appliance := self._appliances.find(f'./appliance[@id="{dev_id}"]')
|
@@ -1032,8 +1070,18 @@ class SmileHelper:
|
|
1032
1070
|
# Collect availability-status for wireless connected devices to Adam
|
1033
1071
|
self._wireless_availablity(appliance, data)
|
1034
1072
|
|
1035
|
-
|
1036
|
-
|
1073
|
+
if dev_id == self.gateway_id and self.smile(ADAM):
|
1074
|
+
self._get_regulation_mode(appliance, data)
|
1075
|
+
|
1076
|
+
# Adam & Anna: the Smile outdoor_temperature is present in DOMAIN_OBJECTS and LOCATIONS - under Home
|
1077
|
+
# The outdoor_temperature present in APPLIANCES is a local sensor connected to the active device
|
1078
|
+
if self._is_thermostat and dev_id == self.gateway_id:
|
1079
|
+
outdoor_temperature = self._object_value(
|
1080
|
+
self._home_location, "outdoor_temperature"
|
1081
|
+
)
|
1082
|
+
if outdoor_temperature is not None:
|
1083
|
+
data.update({"sensors": {"outdoor_temperature": outdoor_temperature}})
|
1084
|
+
self._count += 1
|
1037
1085
|
|
1038
1086
|
if "c_heating_state" in data:
|
1039
1087
|
self._process_c_heating_state(data)
|
@@ -1507,15 +1555,12 @@ class SmileHelper:
|
|
1507
1555
|
Obtain the toggle state of a 'toggle' = switch.
|
1508
1556
|
"""
|
1509
1557
|
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
|
1558
|
+
locator = f"./actuator_functionalities/toggle_functionality[type='{toggle}']/state"
|
1559
|
+
if (state := xml.find(locator)) is not None:
|
1560
|
+
data["switches"][name] = state.text == "on"
|
1561
|
+
self._count += 1
|
1562
|
+
# Remove the cooling_enabled binary_sensor when the corresponding switch is present
|
1563
|
+
# Except for Elga
|
1564
|
+
if toggle == "cooling_enabled" and not self._elga:
|
1565
|
+
data["binary_sensors"].pop("cooling_enabled")
|
1566
|
+
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
|