plugwise 0.37.1a2__tar.gz → 0.37.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.
Files changed (33) hide show
  1. {plugwise-0.37.1a2 → plugwise-0.37.2}/PKG-INFO +1 -4
  2. plugwise-0.37.2/plugwise/common.py +329 -0
  3. {plugwise-0.37.1a2 → plugwise-0.37.2}/plugwise/constants.py +47 -44
  4. {plugwise-0.37.1a2 → plugwise-0.37.2}/plugwise/data.py +57 -73
  5. {plugwise-0.37.1a2 → plugwise-0.37.2}/plugwise/helper.py +430 -614
  6. {plugwise-0.37.1a2 → plugwise-0.37.2}/plugwise/legacy/data.py +28 -44
  7. {plugwise-0.37.1a2 → plugwise-0.37.2}/plugwise/legacy/helper.py +174 -357
  8. {plugwise-0.37.1a2 → plugwise-0.37.2}/plugwise/util.py +62 -0
  9. {plugwise-0.37.1a2 → plugwise-0.37.2}/plugwise.egg-info/PKG-INFO +1 -4
  10. {plugwise-0.37.1a2 → plugwise-0.37.2}/plugwise.egg-info/requires.txt +0 -3
  11. {plugwise-0.37.1a2 → plugwise-0.37.2}/pyproject.toml +2 -5
  12. {plugwise-0.37.1a2 → plugwise-0.37.2}/tests/test_init.py +8 -8
  13. {plugwise-0.37.1a2 → plugwise-0.37.2}/tests/test_legacy_anna.py +0 -2
  14. {plugwise-0.37.1a2 → plugwise-0.37.2}/tests/test_legacy_p1.py +0 -2
  15. plugwise-0.37.1a2/plugwise/common.py +0 -151
  16. {plugwise-0.37.1a2 → plugwise-0.37.2}/LICENSE +0 -0
  17. {plugwise-0.37.1a2 → plugwise-0.37.2}/README.md +0 -0
  18. {plugwise-0.37.1a2 → plugwise-0.37.2}/plugwise/__init__.py +0 -0
  19. {plugwise-0.37.1a2 → plugwise-0.37.2}/plugwise/exceptions.py +0 -0
  20. {plugwise-0.37.1a2 → plugwise-0.37.2}/plugwise/legacy/smile.py +42 -42
  21. {plugwise-0.37.1a2 → plugwise-0.37.2}/plugwise/py.typed +0 -0
  22. {plugwise-0.37.1a2 → plugwise-0.37.2}/plugwise/smile.py +130 -130
  23. {plugwise-0.37.1a2 → plugwise-0.37.2}/plugwise.egg-info/SOURCES.txt +0 -0
  24. {plugwise-0.37.1a2 → plugwise-0.37.2}/plugwise.egg-info/dependency_links.txt +0 -0
  25. {plugwise-0.37.1a2 → plugwise-0.37.2}/plugwise.egg-info/top_level.txt +0 -0
  26. {plugwise-0.37.1a2 → plugwise-0.37.2}/setup.cfg +0 -0
  27. {plugwise-0.37.1a2 → plugwise-0.37.2}/setup.py +0 -0
  28. {plugwise-0.37.1a2 → plugwise-0.37.2}/tests/test_adam.py +0 -0
  29. {plugwise-0.37.1a2 → plugwise-0.37.2}/tests/test_anna.py +0 -0
  30. {plugwise-0.37.1a2 → plugwise-0.37.2}/tests/test_generic.py +0 -0
  31. {plugwise-0.37.1a2 → plugwise-0.37.2}/tests/test_legacy_generic.py +0 -0
  32. {plugwise-0.37.1a2 → plugwise-0.37.2}/tests/test_legacy_stretch.py +0 -0
  33. {plugwise-0.37.1a2 → plugwise-0.37.2}/tests/test_p1.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: plugwise
3
- Version: 0.37.1a2
3
+ Version: 0.37.2
4
4
  Summary: Plugwise Smile (Adam/Anna/P1) and Stretch module for Python 3.
5
5
  Home-page: https://github.com/plugwise/python-plugwise
6
6
  Author: Plugwise device owners
@@ -42,11 +42,8 @@ Requires-Python: >=3.11.0
42
42
  Description-Content-Type: text/markdown
43
43
  License-File: LICENSE
44
44
  Requires-Dist: aiohttp
45
- Requires-Dist: async_timeout
46
- Requires-Dist: crcmod
47
45
  Requires-Dist: defusedxml
48
46
  Requires-Dist: munch
49
- Requires-Dist: pyserial
50
47
  Requires-Dist: python-dateutil
51
48
 
52
49
  # Plugwise python module
@@ -0,0 +1,329 @@
1
+ """Use of this source code is governed by the MIT license found in the LICENSE file.
2
+
3
+ Plugwise Smile protocol helpers.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ from typing import cast
8
+
9
+ from plugwise.constants import (
10
+ ANNA,
11
+ SPECIAL_PLUG_TYPES,
12
+ SWITCH_GROUP_TYPES,
13
+ ApplianceType,
14
+ DeviceData,
15
+ ModelData,
16
+ SensorType,
17
+ )
18
+ from plugwise.util import (
19
+ check_alternative_location,
20
+ check_heater_central,
21
+ check_model,
22
+ get_vendor_name,
23
+ power_data_local_format,
24
+ return_valid,
25
+ )
26
+
27
+ from defusedxml import ElementTree as etree
28
+ from munch import Munch
29
+
30
+
31
+ class SmileCommon:
32
+ """The SmileCommon class."""
33
+
34
+ def __init__(self) -> None:
35
+ """Init."""
36
+ self._appliances: etree
37
+ self._count: int
38
+ self._domain_objects: etree
39
+ self._cooling_present: bool
40
+ self._heater_id: str
41
+ self._on_off_device: bool
42
+ self._opentherm_device: bool
43
+ self.gw_devices: dict[str, DeviceData]
44
+ self.smile_name: str
45
+ self.smile_type: str
46
+
47
+ def smile(self, name: str) -> bool:
48
+ """Helper-function checking the smile-name."""
49
+ return self.smile_name == name
50
+
51
+ def _appl_heater_central_info(
52
+ self,
53
+ appl: Munch,
54
+ xml_1: etree,
55
+ xml_2: etree = None,
56
+ xml_3: etree = None,
57
+ ) -> Munch:
58
+ """Helper-function for _appliance_info_finder()."""
59
+ # Remove heater_central when no active device present
60
+ if not self._opentherm_device and not self._on_off_device:
61
+ return None
62
+
63
+ # Find the valid heater_central
64
+ # xml_2 self._appliances for legacy, self._domain_objects for actual
65
+ xml_2 = return_valid(xml_2, self._domain_objects)
66
+ self._heater_id = check_heater_central(xml_2)
67
+
68
+ # Info for On-Off device
69
+ if self._on_off_device:
70
+ appl.name = "OnOff" # pragma: no cover
71
+ appl.vendor_name = None # pragma: no cover
72
+ appl.model = "Unknown" # pragma: no cover
73
+ return appl # pragma: no cover
74
+
75
+ # Info for OpenTherm device
76
+ appl.name = "OpenTherm"
77
+ locator_1 = "./logs/point_log[type='flame_state']/boiler_state"
78
+ locator_2 = "./services/boiler_state"
79
+ mod_type = "boiler_state"
80
+ # xml_1: appliance
81
+ # xml_3: self._modules for legacy, self._domain_objects for actual
82
+ xml_3 = return_valid(xml_3, self._domain_objects)
83
+ module_data = self._get_module_data(xml_1, locator_1, mod_type, xml_3)
84
+ if not module_data["contents"]:
85
+ module_data = self._get_module_data(xml_1, locator_2, mod_type, xml_3)
86
+ appl.vendor_name = module_data["vendor_name"]
87
+ appl.hardware = module_data["hardware_version"]
88
+ appl.model = module_data["vendor_model"]
89
+ if appl.model is None:
90
+ appl.model = (
91
+ "Generic heater/cooler"
92
+ if self._cooling_present
93
+ else "Generic heater"
94
+ )
95
+
96
+ return appl
97
+
98
+ def _appl_thermostat_info(self, appl: Munch, xml_1: etree, xml_2: etree = None) -> Munch:
99
+ """Helper-function for _appliance_info_finder()."""
100
+ locator = "./logs/point_log[type='thermostat']/thermostat"
101
+ mod_type = "thermostat"
102
+ xml_2 = return_valid(xml_2, self._domain_objects)
103
+ module_data = self._get_module_data(xml_1, locator, mod_type, xml_2)
104
+ appl.vendor_name = module_data["vendor_name"]
105
+ appl.model = check_model(module_data["vendor_model"], appl.vendor_name)
106
+ appl.hardware = module_data["hardware_version"]
107
+ appl.firmware = module_data["firmware_version"]
108
+ appl.zigbee_mac = module_data["zigbee_mac_address"]
109
+
110
+ return appl
111
+
112
+ def _collect_power_values(self, data: DeviceData, loc: Munch, tariff: str, legacy: bool = False) -> None:
113
+ """Something."""
114
+ for loc.peak_select in ("nl_peak", "nl_offpeak"):
115
+ loc.locator = (
116
+ f'./{loc.log_type}[type="{loc.measurement}"]/period/'
117
+ f'measurement[@{tariff}="{loc.peak_select}"]'
118
+ )
119
+ if legacy:
120
+ loc.locator = (
121
+ f"./{loc.meas_list[0]}_{loc.log_type}/measurement"
122
+ f'[@directionality="{loc.meas_list[1]}"][@{tariff}="{loc.peak_select}"]'
123
+ )
124
+
125
+ loc = self._power_data_peak_value(loc, legacy)
126
+ if not loc.found:
127
+ continue
128
+
129
+ data = self._power_data_energy_diff(
130
+ loc.measurement, loc.net_string, loc.f_val, data
131
+ )
132
+ key = cast(SensorType, loc.key_string)
133
+ data["sensors"][key] = loc.f_val
134
+
135
+ def _power_data_peak_value(self, loc: Munch, legacy: bool) -> Munch:
136
+ """Helper-function for _power_data_from_location() and _power_data_from_modules()."""
137
+ loc.found = True
138
+ if loc.logs.find(loc.locator) is None:
139
+ loc = check_alternative_location(loc, legacy)
140
+ if not loc.found:
141
+ return loc
142
+
143
+ if (peak := loc.peak_select.split("_")[1]) == "offpeak":
144
+ peak = "off_peak"
145
+ log_found = loc.log_type.split("_")[0]
146
+ loc.key_string = f"{loc.measurement}_{peak}_{log_found}"
147
+ if "gas" in loc.measurement or loc.log_type == "point_meter":
148
+ loc.key_string = f"{loc.measurement}_{log_found}"
149
+ # Only for P1 Actual -------------------#
150
+ if "phase" in loc.measurement:
151
+ loc.key_string = f"{loc.measurement}"
152
+ # --------------------------------------#
153
+ loc.net_string = f"net_electricity_{log_found}"
154
+ val = loc.logs.find(loc.locator).text
155
+ loc.f_val = power_data_local_format(loc.attrs, loc.key_string, val)
156
+
157
+ return loc
158
+
159
+ def _power_data_energy_diff(
160
+ self,
161
+ measurement: str,
162
+ net_string: SensorType,
163
+ f_val: float | int,
164
+ direct_data: DeviceData,
165
+ ) -> DeviceData:
166
+ """Calculate differential energy."""
167
+ if (
168
+ "electricity" in measurement
169
+ and "phase" not in measurement
170
+ and "interval" not in net_string
171
+ ):
172
+ diff = 1
173
+ if "produced" in measurement:
174
+ diff = -1
175
+ if net_string not in direct_data["sensors"]:
176
+ tmp_val: float | int = 0
177
+ else:
178
+ tmp_val = direct_data["sensors"][net_string]
179
+
180
+ if isinstance(f_val, int):
181
+ tmp_val += f_val * diff
182
+ else:
183
+ tmp_val += float(f_val * diff)
184
+ tmp_val = float(f"{round(tmp_val, 3):.3f}")
185
+
186
+ direct_data["sensors"][net_string] = tmp_val
187
+
188
+ return direct_data
189
+
190
+ def _create_gw_devices(self, appl: Munch) -> None:
191
+ """Helper-function for creating/updating gw_devices."""
192
+ self.gw_devices[appl.dev_id] = {"dev_class": appl.pwclass}
193
+ self._count += 1
194
+ for key, value in {
195
+ "firmware": appl.firmware,
196
+ "hardware": appl.hardware,
197
+ "location": appl.location,
198
+ "mac_address": appl.mac,
199
+ "model": appl.model,
200
+ "name": appl.name,
201
+ "zigbee_mac_address": appl.zigbee_mac,
202
+ "vendor": appl.vendor_name,
203
+ }.items():
204
+ if value is not None or key == "location":
205
+ appl_key = cast(ApplianceType, key)
206
+ self.gw_devices[appl.dev_id][appl_key] = value
207
+ self._count += 1
208
+
209
+ def _device_data_switching_group(
210
+ self, device: DeviceData, data: DeviceData
211
+ ) -> None:
212
+ """Helper-function for _get_device_data().
213
+
214
+ Determine switching group device data.
215
+ """
216
+ if device["dev_class"] in SWITCH_GROUP_TYPES:
217
+ counter = 0
218
+ for member in device["members"]:
219
+ if self.gw_devices[member]["switches"].get("relay"):
220
+ counter += 1
221
+ data["switches"]["relay"] = counter != 0
222
+ self._count += 1
223
+
224
+ def _get_group_switches(self) -> dict[str, DeviceData]:
225
+ """Helper-function for smile.py: get_all_devices().
226
+
227
+ Collect switching- or pump-group info.
228
+ """
229
+ switch_groups: dict[str, DeviceData] = {}
230
+ # P1 and Anna don't have switchgroups
231
+ if self.smile_type == "power" or self.smile(ANNA):
232
+ return switch_groups
233
+
234
+ for group in self._domain_objects.findall("./group"):
235
+ members: list[str] = []
236
+ group_id = group.attrib["id"]
237
+ group_name = group.find("name").text
238
+ group_type = group.find("type").text
239
+ group_appliances = group.findall("appliances/appliance")
240
+ for item in group_appliances:
241
+ # Check if members are not orphaned - stretch
242
+ if item.attrib["id"] in self.gw_devices:
243
+ members.append(item.attrib["id"])
244
+
245
+ if group_type in SWITCH_GROUP_TYPES and members:
246
+ switch_groups.update(
247
+ {
248
+ group_id: {
249
+ "dev_class": group_type,
250
+ "model": "Switchgroup",
251
+ "name": group_name,
252
+ "members": members,
253
+ },
254
+ },
255
+ )
256
+ self._count += 4
257
+
258
+ return switch_groups
259
+
260
+ def _get_lock_state(self, xml: etree, data: DeviceData, stretch_v2: bool = False) -> None:
261
+ """Helper-function for _get_measurement_data().
262
+
263
+ Adam & Stretches: obtain the relay-switch lock state.
264
+ """
265
+ actuator = "actuator_functionalities"
266
+ func_type = "relay_functionality"
267
+ if stretch_v2:
268
+ actuator = "actuators"
269
+ func_type = "relay"
270
+ if xml.find("type").text not in SPECIAL_PLUG_TYPES:
271
+ locator = f"./{actuator}/{func_type}/lock"
272
+ if (found := xml.find(locator)) is not None:
273
+ data["switches"]["lock"] = found.text == "true"
274
+ self._count += 1
275
+
276
+ def _get_module_data(
277
+ self,
278
+ xml_1: etree,
279
+ locator: str,
280
+ mod_type: str,
281
+ xml_2: etree = None,
282
+ legacy: bool = False,
283
+ ) -> ModelData:
284
+ """Helper-function for _energy_device_info_finder() and _appliance_info_finder().
285
+
286
+ Collect requested info from MODULES.
287
+ """
288
+ model_data: ModelData = {
289
+ "contents": False,
290
+ "firmware_version": None,
291
+ "hardware_version": None,
292
+ "reachable": None,
293
+ "vendor_name": None,
294
+ "vendor_model": None,
295
+ "zigbee_mac_address": None,
296
+ }
297
+ # xml_1: appliance
298
+ if (appl_search := xml_1.find(locator)) is not None:
299
+ link_id = appl_search.attrib["id"]
300
+ loc = f".//services/{mod_type}[@id='{link_id}']...."
301
+ if legacy:
302
+ loc = f".//{mod_type}[@id='{link_id}']...."
303
+ # Not possible to walrus for some reason...
304
+ # xml_2: self._modules for legacy, self._domain_objects for actual
305
+ search = return_valid(xml_2, self._domain_objects)
306
+ module = search.find(loc)
307
+ if module is not None: # pylint: disable=consider-using-assignment-expr
308
+ model_data["contents"] = True
309
+ get_vendor_name(module, model_data)
310
+ model_data["vendor_model"] = module.find("vendor_model").text
311
+ model_data["hardware_version"] = module.find("hardware_version").text
312
+ model_data["firmware_version"] = module.find("firmware_version").text
313
+ self._get_zigbee_data(module, model_data, legacy)
314
+
315
+ return model_data
316
+
317
+ def _get_zigbee_data(self, module: etree, model_data: ModelData, legacy: bool) -> None:
318
+ """Helper-function for _get_model_data()."""
319
+ if legacy:
320
+ # Stretches
321
+ if (router := module.find("./protocols/network_router")) is not None:
322
+ model_data["zigbee_mac_address"] = router.find("mac_address").text
323
+ # Also look for the Circle+/Stealth M+
324
+ if (coord := module.find("./protocols/network_coordinator")) is not None:
325
+ model_data["zigbee_mac_address"] = coord.find("mac_address").text
326
+ # Adam
327
+ elif (zb_node := module.find("./protocols/zig_bee_node")) is not None:
328
+ model_data["zigbee_mac_address"] = zb_node.find("mac_address").text
329
+ model_data["reachable"] = zb_node.find("reachable").text == "true"
@@ -121,21 +121,20 @@ P1_LEGACY_MEASUREMENTS: Final[dict[str, UOM]] = {
121
121
  # radiator_valve: 'uncorrected_temperature', 'temperature_offset'
122
122
 
123
123
  DEVICE_MEASUREMENTS: Final[dict[str, DATA | UOM]] = {
124
- # HA Core thermostat current_temperature
125
- "temperature": UOM(TEMP_CELSIUS),
126
- # HA Core thermostat setpoint
127
- "thermostat": DATA("setpoint", TEMP_CELSIUS),
128
- # Specific for an Anna
129
- "illuminance": UOM(UNIT_LUMEN),
124
+ "humidity": UOM(PERCENTAGE), # Specific for a Jip
125
+ "illuminance": UOM(UNIT_LUMEN), # Specific for an Anna
126
+ "temperature": UOM(TEMP_CELSIUS), # HA Core thermostat current_temperature
127
+ "thermostat": DATA("setpoint", TEMP_CELSIUS), # HA Core thermostat setpoint
128
+ ########################################################
130
129
  # Specific for an Anna with heatpump extension installed
131
130
  "cooling_activation_outdoor_temperature": UOM(TEMP_CELSIUS),
132
131
  "cooling_deactivation_threshold": UOM(TEMP_CELSIUS),
133
- # Specific for a Lisa a Tom/Floor
132
+ ##################################
133
+ # Specific for a Lisa or Tom/Floor
134
134
  "battery": UOM(PERCENTAGE),
135
135
  "temperature_difference": UOM(DEGREE),
136
136
  "valve_position": UOM(PERCENTAGE),
137
- # Specific for a Jip
138
- "humidity": UOM(PERCENTAGE),
137
+ #####################
139
138
  # Specific for a Plug
140
139
  "electricity_consumed": UOM(POWER_WATT),
141
140
  "electricity_produced": UOM(POWER_WATT),
@@ -144,39 +143,43 @@ DEVICE_MEASUREMENTS: Final[dict[str, DATA | UOM]] = {
144
143
 
145
144
  # Heater Central related measurements
146
145
  HEATER_CENTRAL_MEASUREMENTS: Final[dict[str, DATA | UOM]] = {
146
+ "boiler_state": DATA(
147
+ "flame_state", NONE
148
+ ), # Legacy Anna: similar to flame-state on Anna/Adam
147
149
  "boiler_temperature": DATA("water_temperature", TEMP_CELSIUS),
150
+ "central_heating_state": DATA(
151
+ "c_heating_state", NONE
152
+ ), # For Elga (heatpump) use this instead of intended_central_heating_state
153
+ "central_heater_water_pressure": DATA("water_pressure", PRESSURE_BAR),
154
+ "compressor_state": UOM(NONE), # present with heatpump
155
+ "cooling_enabled": UOM(
156
+ NONE
157
+ ), # Available with the Loria and Elga (newer Anna firmware) heatpumps
158
+ "cooling_state": UOM(NONE),
148
159
  "domestic_hot_water_mode": DATA("select_dhw_mode", NONE),
149
160
  "domestic_hot_water_setpoint": UOM(TEMP_CELSIUS),
150
161
  "domestic_hot_water_state": DATA("dhw_state", NONE),
151
162
  "domestic_hot_water_temperature": DATA("dhw_temperature", TEMP_CELSIUS),
152
163
  "elga_status_code": UOM(NONE),
164
+ "intended_boiler_state": DATA(
165
+ "heating_state", NONE
166
+ ), # Legacy Anna: shows when heating is active, we don't show dhw_state, cannot be determined reliably
167
+ "flame_state": UOM(
168
+ NONE
169
+ ), # Also present when there is a single gas-heater
153
170
  "intended_boiler_temperature": UOM(
154
171
  TEMP_CELSIUS
155
172
  ), # Non-zero when heating, zero when dhw-heating
156
- "central_heating_state": DATA(
157
- "c_heating_state", NONE
158
- ), # For Elga (heatpump) use this instead of intended_central_heating_state
159
173
  "intended_central_heating_state": DATA(
160
174
  "heating_state", NONE
161
175
  ), # This key shows in general the heating-behavior better than c-h_state. except when connected to a heatpump
162
176
  "modulation_level": UOM(PERCENTAGE),
163
177
  "return_water_temperature": DATA("return_temperature", TEMP_CELSIUS),
164
- # Used with the Elga heatpump - marcelveldt
165
- "compressor_state": UOM(NONE),
166
- "cooling_state": UOM(NONE),
167
- "thermostat_supports_cooling": UOM(NONE),
168
- # Available with the Loria and Elga (newer Anna firmware) heatpumps
169
- "cooling_enabled": UOM(NONE),
170
- # Next 2 keys are used to show the state of the gas-heater used next to the Elga heatpump - marcelveldt
171
- "slave_boiler_state": UOM(NONE),
172
- "flame_state": UOM(NONE), # Also present when there is a single gas-heater
173
- "central_heater_water_pressure": DATA("water_pressure", PRESSURE_BAR),
174
- # Legacy Anna: similar to flame-state on Anna/Adam
175
- "boiler_state": DATA("flame_state", NONE),
176
- # Legacy Anna: shows when heating is active, we don't show dhw_state, cannot be determined reliably
177
- "intended_boiler_state": DATA("heating_state", NONE),
178
- # Outdoor temperature from APPLIANCES - present for a heatpump
179
- "outdoor_temperature": DATA("outdoor_air_temperature", TEMP_CELSIUS),
178
+ "outdoor_temperature": DATA(
179
+ "outdoor_air_temperature", TEMP_CELSIUS
180
+ ), # Outdoor temperature from APPLIANCES - present for a heatpump
181
+ "slave_boiler_state": DATA("secondary_boiler_state", NONE),
182
+ "thermostat_supports_cooling": UOM(NONE), # present with heatpump
180
183
  }
181
184
 
182
185
  OBSOLETE_MEASUREMENTS: Final[tuple[str, ...]] = (
@@ -253,22 +256,22 @@ ApplianceType = Literal[
253
256
  ]
254
257
 
255
258
  BinarySensorType = Literal[
256
- "cooling_enabled",
257
259
  "compressor_state",
260
+ "cooling_enabled",
258
261
  "cooling_state",
259
262
  "dhw_state",
260
263
  "flame_state",
261
264
  "heating_state",
262
265
  "plugwise_notification",
263
- "slave_boiler_state",
266
+ "secondary_boiler_state",
264
267
  ]
265
268
  BINARY_SENSORS: Final[tuple[str, ...]] = get_args(BinarySensorType)
266
269
 
267
270
  LIMITS: Final[tuple[str, ...]] = (
271
+ "lower_bound",
268
272
  "offset",
269
- "setpoint",
270
273
  "resolution",
271
- "lower_bound",
274
+ "setpoint",
272
275
  "upper_bound",
273
276
  )
274
277
 
@@ -329,8 +332,8 @@ SENSORS: Final[tuple[str, ...]] = get_args(SensorType)
329
332
 
330
333
  SPECIAL_PLUG_TYPES: Final[tuple[str, ...]] = (
331
334
  "central_heating_pump",
332
- "valve_actuator",
333
335
  "heater_electric",
336
+ "valve_actuator",
334
337
  )
335
338
 
336
339
  SpecialType = Literal[
@@ -350,14 +353,14 @@ SwitchType = Literal[
350
353
  ]
351
354
  SWITCHES: Final[tuple[str, ...]] = get_args(SwitchType)
352
355
 
353
- SWITCH_GROUP_TYPES: Final[tuple[str, ...]] = ("switching", "report")
356
+ SWITCH_GROUP_TYPES: Final[tuple[str, ...]] = ("report", "switching")
354
357
 
355
358
  THERMOSTAT_CLASSES: Final[tuple[str, ...]] = (
356
359
  "thermostat",
360
+ "thermostatic_radiator_valve",
357
361
  "thermo_sensor",
358
362
  "zone_thermometer",
359
363
  "zone_thermostat",
360
- "thermostatic_radiator_valve",
361
364
  )
362
365
 
363
366
  ToggleNameType = Literal[
@@ -392,25 +395,25 @@ class ModelData(TypedDict):
392
395
  """The ModelData class."""
393
396
 
394
397
  contents: bool
395
- vendor_name: str | None
396
- vendor_model: str | None
397
- hardware_version: str | None
398
398
  firmware_version: str | None
399
- zigbee_mac_address: str | None
399
+ hardware_version: str | None
400
400
  reachable: bool | None
401
+ vendor_model: str | None
402
+ vendor_name: str | None
403
+ zigbee_mac_address: str | None
401
404
 
402
405
 
403
406
  class SmileBinarySensors(TypedDict, total=False):
404
407
  """Smile Binary Sensors class."""
405
408
 
406
- cooling_enabled: bool
407
409
  compressor_state: bool
410
+ cooling_enabled: bool
408
411
  cooling_state: bool
409
412
  dhw_state: bool
410
413
  flame_state: bool
411
414
  heating_state: bool
412
415
  plugwise_notification: bool
413
- slave_boiler_state: bool
416
+ secondary_boiler_state: bool
414
417
 
415
418
 
416
419
  class SmileSensors(TypedDict, total=False):
@@ -482,9 +485,9 @@ class ThermoLoc(TypedDict, total=False):
482
485
  """Thermo Location class."""
483
486
 
484
487
  name: str
485
- master: str | None
486
- master_prio: int
487
- slaves: set[str]
488
+ primary: str | None
489
+ primary_prio: int
490
+ secondary: set[str]
488
491
 
489
492
 
490
493
  class ActuatorData(TypedDict, total=False):