plugwise 0.36.2__tar.gz → 0.37.0__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 (32) hide show
  1. {plugwise-0.36.2 → plugwise-0.37.0}/PKG-INFO +1 -1
  2. plugwise-0.37.0/plugwise/__init__.py +391 -0
  3. plugwise-0.37.0/plugwise/data.py +263 -0
  4. {plugwise-0.36.2 → plugwise-0.37.0}/plugwise/helper.py +141 -381
  5. plugwise-0.37.0/plugwise/legacy/data.py +124 -0
  6. plugwise-0.37.0/plugwise/legacy/helper.py +775 -0
  7. plugwise-0.37.0/plugwise/legacy/smile.py +272 -0
  8. plugwise-0.37.0/plugwise/smile.py +426 -0
  9. {plugwise-0.36.2 → plugwise-0.37.0}/plugwise/util.py +35 -1
  10. {plugwise-0.36.2 → plugwise-0.37.0}/plugwise.egg-info/PKG-INFO +1 -1
  11. {plugwise-0.36.2 → plugwise-0.37.0}/plugwise.egg-info/SOURCES.txt +10 -2
  12. {plugwise-0.36.2 → plugwise-0.37.0}/pyproject.toml +12 -11
  13. {plugwise-0.36.2 → plugwise-0.37.0}/tests/test_adam.py +12 -41
  14. {plugwise-0.36.2 → plugwise-0.37.0}/tests/test_anna.py +36 -150
  15. {plugwise-0.36.2 → plugwise-0.37.0}/tests/test_generic.py +0 -11
  16. {plugwise-0.36.2 → plugwise-0.37.0}/tests/test_init.py +242 -19
  17. plugwise-0.37.0/tests/test_legacy_anna.py +80 -0
  18. plugwise-0.37.0/tests/test_legacy_generic.py +22 -0
  19. plugwise-0.37.0/tests/test_legacy_p1.py +71 -0
  20. plugwise-0.36.2/tests/test_stretch.py → plugwise-0.37.0/tests/test_legacy_stretch.py +11 -6
  21. {plugwise-0.36.2 → plugwise-0.37.0}/tests/test_p1.py +3 -62
  22. plugwise-0.36.2/plugwise/__init__.py +0 -917
  23. {plugwise-0.36.2 → plugwise-0.37.0}/LICENSE +0 -0
  24. {plugwise-0.36.2 → plugwise-0.37.0}/README.md +0 -0
  25. {plugwise-0.36.2 → plugwise-0.37.0}/plugwise/constants.py +0 -0
  26. {plugwise-0.36.2 → plugwise-0.37.0}/plugwise/exceptions.py +0 -0
  27. {plugwise-0.36.2 → plugwise-0.37.0}/plugwise/py.typed +0 -0
  28. {plugwise-0.36.2 → plugwise-0.37.0}/plugwise.egg-info/dependency_links.txt +0 -0
  29. {plugwise-0.36.2 → plugwise-0.37.0}/plugwise.egg-info/requires.txt +0 -0
  30. {plugwise-0.36.2 → plugwise-0.37.0}/plugwise.egg-info/top_level.txt +0 -0
  31. {plugwise-0.36.2 → plugwise-0.37.0}/setup.cfg +0 -0
  32. {plugwise-0.36.2 → plugwise-0.37.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: plugwise
3
- Version: 0.36.2
3
+ Version: 0.37.0
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
@@ -0,0 +1,391 @@
1
+ """Use of this source code is governed by the MIT license found in the LICENSE file.
2
+
3
+ Plugwise backend module for Home Assistant Core.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ from plugwise.constants import (
8
+ DEFAULT_PORT,
9
+ DEFAULT_TIMEOUT,
10
+ DEFAULT_USERNAME,
11
+ DOMAIN_OBJECTS,
12
+ LOGGER,
13
+ MODULES,
14
+ NONE,
15
+ SMILES,
16
+ STATUS,
17
+ SYSTEM,
18
+ PlugwiseData,
19
+ ThermoLoc,
20
+ )
21
+ from plugwise.exceptions import (
22
+ InvalidSetupError,
23
+ PlugwiseError,
24
+ ResponseError,
25
+ UnsupportedDeviceError,
26
+ )
27
+ from plugwise.helper import SmileComm
28
+ from plugwise.legacy.smile import SmileLegacyAPI
29
+ from plugwise.smile import SmileAPI
30
+
31
+ import aiohttp
32
+ from defusedxml import ElementTree as etree
33
+
34
+ # Dict as class
35
+ # Version detection
36
+ import semver
37
+
38
+
39
+ class Smile(SmileComm):
40
+ """The Plugwise SmileConnect class."""
41
+
42
+ # pylint: disable=too-many-instance-attributes, too-many-public-methods
43
+
44
+ def __init__(
45
+ self,
46
+ host: str,
47
+ password: str,
48
+ websession: aiohttp.ClientSession,
49
+ username: str = DEFAULT_USERNAME,
50
+ port: int = DEFAULT_PORT,
51
+ timeout: float = DEFAULT_TIMEOUT,
52
+
53
+ ) -> None:
54
+ """Set the constructor for this class."""
55
+ super().__init__(
56
+ host,
57
+ password,
58
+ websession,
59
+ username,
60
+ port,
61
+ timeout,
62
+ )
63
+
64
+ self._host = host
65
+ self._passwd = password
66
+ self._websession = websession
67
+ self._user = username
68
+ self._port = port
69
+ self._timeout = timeout
70
+
71
+ self._cooling_present = False
72
+ self._elga = False
73
+ self._is_thermostat = False
74
+ self._last_active: dict[str, str | None] = {}
75
+ self._on_off_device = False
76
+ self._opentherm_device = False
77
+ self._schedule_old_states: dict[str, dict[str, str]] = {}
78
+ self._smile_api: SmileAPI | SmileLegacyAPI
79
+ self._stretch_v2 = False
80
+ self._target_smile: str = NONE
81
+ self.gateway_id: str = NONE
82
+ self.loc_data: dict[str, ThermoLoc] = {}
83
+ self.smile_fw_version: str | None
84
+ self.smile_hostname: str
85
+ self.smile_hw_version: str | None = None
86
+ self.smile_legacy = False
87
+ self.smile_mac_address: str | None
88
+ self.smile_model: str
89
+ self.smile_name: str
90
+ self.smile_type: str
91
+ self.smile_version: tuple[str, semver.version.Version]
92
+ self.smile_zigbee_mac_address: str | None = None
93
+
94
+ async def connect(self) -> bool:
95
+ """Connect to Plugwise device and determine its name, type and version."""
96
+ result = await self._request(DOMAIN_OBJECTS)
97
+ # Work-around for Stretch fw 2.7.18
98
+ if not (vendor_names := result.findall("./module/vendor_name")):
99
+ result = await self._request(MODULES)
100
+ vendor_names = result.findall("./module/vendor_name")
101
+
102
+ names: list[str] = []
103
+ for name in vendor_names:
104
+ names.append(name.text)
105
+
106
+ vendor_models = result.findall("./module/vendor_model")
107
+ models: list[str] = []
108
+ for model in vendor_models:
109
+ models.append(model.text)
110
+
111
+ dsmrmain = result.find("./module/protocols/dsmrmain")
112
+ if "Plugwise" not in names and dsmrmain is None: # pragma: no cover
113
+ LOGGER.error(
114
+ "Connected but expected text not returned, we got %s. Please create"
115
+ " an issue on http://github.com/plugwise/python-plugwise",
116
+ result,
117
+ )
118
+ raise ResponseError
119
+
120
+ # Check if Anna is connected to an Adam
121
+ if "159.2" in models:
122
+ LOGGER.error(
123
+ "Your Anna is connected to an Adam, make sure to only add the Adam as integration."
124
+ )
125
+ raise InvalidSetupError
126
+
127
+ # Determine smile specifics
128
+ await self._smile_detect(result, dsmrmain)
129
+
130
+ self._smile_api = SmileAPI(
131
+ self._host,
132
+ self._passwd,
133
+ self._websession,
134
+ self._cooling_present,
135
+ self._elga,
136
+ self._is_thermostat,
137
+ self._last_active,
138
+ self._on_off_device,
139
+ self._opentherm_device,
140
+ self._schedule_old_states,
141
+ self.gateway_id,
142
+ self.loc_data,
143
+ self.smile_fw_version,
144
+ self.smile_hostname,
145
+ self.smile_hw_version,
146
+ self.smile_legacy,
147
+ self.smile_mac_address,
148
+ self.smile_model,
149
+ self.smile_name,
150
+ self.smile_type,
151
+ self.smile_version,
152
+ self._user,
153
+ self._port,
154
+ self._timeout,
155
+ )
156
+ if self.smile_legacy:
157
+ self._smile_api = SmileLegacyAPI(
158
+ self._host,
159
+ self._passwd,
160
+ self._websession,
161
+ self._is_thermostat,
162
+ self._on_off_device,
163
+ self._opentherm_device,
164
+ self._stretch_v2,
165
+ self._target_smile,
166
+ self.loc_data,
167
+ self.smile_fw_version,
168
+ self.smile_hostname,
169
+ self.smile_hw_version,
170
+ self.smile_mac_address,
171
+ self.smile_model,
172
+ self.smile_name,
173
+ self.smile_type,
174
+ self.smile_version,
175
+ self.smile_zigbee_mac_address,
176
+ self._user,
177
+ self._port,
178
+ self._timeout,
179
+ )
180
+
181
+ # Update all endpoints on first connect
182
+ await self._smile_api.full_update_device()
183
+
184
+ return True
185
+
186
+ async def _smile_detect(self, result: etree, dsmrmain: etree) -> None:
187
+ """Helper-function for connect().
188
+
189
+ Detect which type of Smile is connected.
190
+ """
191
+ model: str = "Unknown"
192
+ if (gateway := result.find("./gateway")) is not None:
193
+ if (v_model := gateway.find("vendor_model")) is not None:
194
+ model = v_model.text
195
+ self.smile_fw_version = gateway.find("firmware_version").text
196
+ self.smile_hw_version = gateway.find("hardware_version").text
197
+ self.smile_hostname = gateway.find("hostname").text
198
+ self.smile_mac_address = gateway.find("mac_address").text
199
+ else:
200
+ model = await self._smile_detect_legacy(result, dsmrmain, model)
201
+
202
+ if model == "Unknown" or self.smile_fw_version is None: # pragma: no cover
203
+ # Corner case check
204
+ LOGGER.error(
205
+ "Unable to find model or version information, please create"
206
+ " an issue on http://github.com/plugwise/python-plugwise"
207
+ )
208
+ raise UnsupportedDeviceError
209
+
210
+ ver = semver.version.Version.parse(self.smile_fw_version)
211
+ self._target_smile = f"{model}_v{ver.major}"
212
+ LOGGER.debug("Plugwise identified as %s", self._target_smile)
213
+ if self._target_smile not in SMILES:
214
+ LOGGER.error(
215
+ "Your Smile identified as %s seems unsupported by our plugin, please"
216
+ " create an issue on http://github.com/plugwise/python-plugwise",
217
+ self._target_smile,
218
+ )
219
+ raise UnsupportedDeviceError
220
+
221
+ if self._target_smile in ("smile_open_therm_v2", "smile_thermo_v3"):
222
+ LOGGER.error(
223
+ "Your Smile identified as %s needs a firmware update as it's firmware is severely outdated",
224
+ self._target_smile,
225
+ ) # pragma: no cover
226
+ raise UnsupportedDeviceError # pragma: no cover
227
+
228
+ self.smile_model = "Gateway"
229
+ self.smile_name = SMILES[self._target_smile].smile_name
230
+ self.smile_type = SMILES[self._target_smile].smile_type
231
+ self.smile_version = (self.smile_fw_version, ver)
232
+
233
+ if self.smile_type == "stretch":
234
+ self._stretch_v2 = self.smile_version[1].major == 2
235
+
236
+ if self.smile_type == "thermostat":
237
+ self._is_thermostat = True
238
+ # For Adam, Anna, determine the system capabilities:
239
+ # Find the connected heating/cooling device (heater_central),
240
+ # e.g. heat-pump or gas-fired heater
241
+ onoff_boiler: etree = result.find("./module/protocols/onoff_boiler")
242
+ open_therm_boiler: etree = result.find(
243
+ "./module/protocols/open_therm_boiler"
244
+ )
245
+ self._on_off_device = onoff_boiler is not None
246
+ self._opentherm_device = open_therm_boiler is not None
247
+
248
+ # Determine the presence of special features
249
+ locator_1 = "./gateway/features/cooling"
250
+ locator_2 = "./gateway/features/elga_support"
251
+ if result.find(locator_1) is not None:
252
+ self._cooling_present = True
253
+ if result.find(locator_2) is not None:
254
+ self._elga = True
255
+
256
+ async def _smile_detect_legacy(
257
+ self, result: etree, dsmrmain: etree, model: str
258
+ ) -> str:
259
+ """Helper-function for _smile_detect()."""
260
+ return_model = model
261
+ # Stretch: find the MAC of the zigbee master_controller (= Stick)
262
+ if (network := result.find("./module/protocols/master_controller")) is not None:
263
+ self.smile_zigbee_mac_address = network.find("mac_address").text
264
+ # Find the active MAC in case there is an orphaned Stick
265
+ if zb_networks := result.findall("./network"):
266
+ for zb_network in zb_networks:
267
+ if zb_network.find("./nodes/network_router") is not None:
268
+ network = zb_network.find("./master_controller")
269
+ self.smile_zigbee_mac_address = network.find("mac_address").text
270
+
271
+ # Legacy Anna or Stretch:
272
+ if (
273
+ result.find('./appliance[type="thermostat"]') is not None
274
+ or network is not None
275
+ ):
276
+ system = await self._request(SYSTEM)
277
+ self.smile_fw_version = system.find("./gateway/firmware").text
278
+ return_model = system.find("./gateway/product").text
279
+ self.smile_hostname = system.find("./gateway/hostname").text
280
+ # If wlan0 contains data it's active, so eth0 should be checked last
281
+ for network in ("wlan0", "eth0"):
282
+ locator = f"./{network}/mac"
283
+ if (net_locator := system.find(locator)) is not None:
284
+ self.smile_mac_address = net_locator.text
285
+ # P1 legacy:
286
+ elif dsmrmain is not None:
287
+ status = await self._request(STATUS)
288
+ self.smile_fw_version = status.find("./system/version").text
289
+ return_model = status.find("./system/product").text
290
+ self.smile_hostname = status.find("./network/hostname").text
291
+ self.smile_mac_address = status.find("./network/mac_address").text
292
+ else: # pragma: no cover
293
+ # No cornercase, just end of the line
294
+ LOGGER.error(
295
+ "Connected but no gateway device information found, please create"
296
+ " an issue on http://github.com/plugwise/python-plugwise"
297
+ )
298
+ raise ResponseError
299
+
300
+ self.smile_legacy = True
301
+ return return_model
302
+
303
+ async def full_update_device(self) -> None:
304
+ """Perform a first fetch of all XML data, needed for initialization."""
305
+ await self._smile_api.full_update_device()
306
+
307
+ def get_all_devices(self) -> None:
308
+ """Determine the devices present from the obtained XML-data."""
309
+ self._smile_api.get_all_devices()
310
+
311
+ async def async_update(self) -> PlugwiseData:
312
+ """Perform an incremental update for updating the various device states."""
313
+ data: PlugwiseData = await self._smile_api.async_update()
314
+ self.gateway_id = data.gateway["gateway_id"]
315
+ return data
316
+
317
+ ########################################################################################################
318
+ ### API Set and HA Service-related Functions ###
319
+ ########################################################################################################
320
+
321
+ async def set_schedule_state(
322
+ self,
323
+ loc_id: str,
324
+ new_state: str,
325
+ name: str | None = None,
326
+ ) -> None:
327
+ """Activate/deactivate the Schedule, with the given name, on the relevant Thermostat."""
328
+ await self._smile_api.set_schedule_state(loc_id, new_state, name)
329
+
330
+ async def set_preset(self, loc_id: str, preset: str) -> None:
331
+ """Set the given Preset on the relevant Thermostat."""
332
+ await self._smile_api.set_preset(loc_id, preset)
333
+
334
+ async def set_temperature(self, loc_id: str, items: dict[str, float]) -> None:
335
+ """Set the given Temperature on the relevant Thermostat."""
336
+ try:
337
+ await self._smile_api.set_temperature(loc_id, items)
338
+ except PlugwiseError as exc:
339
+ raise PlugwiseError(
340
+ "Plugwise: failed setting temperature: no valid input provided"
341
+ ) from exc
342
+
343
+ async def set_number_setpoint(self, key: str, _: str, temperature: float) -> None:
344
+ """Set the max. Boiler or DHW setpoint on the Central Heating boiler."""
345
+ try:
346
+ await self._smile_api.set_number_setpoint(key, temperature)
347
+ except PlugwiseError as exc:
348
+ raise PlugwiseError(f"Plugwise: cannot change setpoint, {key} not found.") from exc
349
+
350
+ async def set_temperature_offset(self, _: str, dev_id: str, offset: float) -> None:
351
+ """Set the Temperature offset for thermostats that support this feature."""
352
+ try:
353
+ await self._smile_api.set_temperature_offset(dev_id, offset)
354
+ except PlugwiseError as exc:
355
+ raise PlugwiseError(
356
+ "Plugwise: this device does not have temperature-offset capability."
357
+ ) from exc
358
+
359
+ async def set_switch_state(
360
+ self, appl_id: str, members: list[str] | None, model: str, state: str
361
+ ) -> None:
362
+ """Set the given State of the relevant Switch."""
363
+ try:
364
+ await self._smile_api.set_switch_state(appl_id, members, model, state)
365
+ except PlugwiseError as exc:
366
+ raise PlugwiseError("Plugwise: the locked Relay was not switched.") from exc
367
+
368
+ async def set_gateway_mode(self, mode: str) -> None:
369
+ """Set the gateway mode."""
370
+ try:
371
+ await self._smile_api.set_gateway_mode(mode)
372
+ except PlugwiseError as exc:
373
+ raise PlugwiseError("Plugwise: invalid gateway mode.") from exc
374
+
375
+ async def set_regulation_mode(self, mode: str) -> None:
376
+ """Set the heating regulation mode."""
377
+ try:
378
+ await self._smile_api.set_regulation_mode(mode)
379
+ except PlugwiseError as exc:
380
+ raise PlugwiseError("Plugwise: invalid regulation mode.") from exc
381
+
382
+ async def set_dhw_mode(self, mode: str) -> None:
383
+ """Set the domestic hot water heating regulation mode."""
384
+ try:
385
+ await self._smile_api.set_dhw_mode(mode)
386
+ except PlugwiseError as exc:
387
+ raise PlugwiseError("Plugwise: invalid dhw mode.") from exc
388
+
389
+ async def delete_notification(self) -> None:
390
+ """Delete the active Plugwise Notification."""
391
+ await self._smile_api.delete_notification()
@@ -0,0 +1,263 @@
1
+ """Use of this source code is governed by the MIT license found in the LICENSE file.
2
+
3
+ Plugwise Smile protocol data-collection helpers.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ from plugwise.constants import (
8
+ ADAM,
9
+ ANNA,
10
+ MAX_SETPOINT,
11
+ MIN_SETPOINT,
12
+ NONE,
13
+ OFF,
14
+ SWITCH_GROUP_TYPES,
15
+ ZONE_THERMOSTATS,
16
+ ActuatorData,
17
+ DeviceData,
18
+ )
19
+ from plugwise.helper import SmileHelper
20
+ from plugwise.util import remove_empty_platform_dicts
21
+
22
+
23
+ class SmileData(SmileHelper):
24
+ """The Plugwise Smile main class."""
25
+
26
+ def __init__(self) -> None:
27
+ """Init."""
28
+ SmileHelper.__init__(self)
29
+
30
+
31
+ def _update_gw_devices(self) -> None:
32
+ """Helper-function for _all_device_data() and async_update().
33
+
34
+ Collect data for each device and add to self.gw_devices.
35
+ """
36
+ for device_id, device in self.gw_devices.items():
37
+ data = self._get_device_data(device_id)
38
+ self._add_or_update_notifications(device_id, device, data)
39
+ device.update(data)
40
+ self._update_for_cooling(device)
41
+ remove_empty_platform_dicts(device)
42
+
43
+ def _add_or_update_notifications(
44
+ self, device_id: str, device: DeviceData, data: DeviceData
45
+ ) -> None:
46
+ """Helper-function adding or updating the Plugwise notifications."""
47
+ if (
48
+ device_id == self.gateway_id
49
+ and (
50
+ self._is_thermostat or self.smile_type == "power"
51
+ )
52
+ ) or (
53
+ "binary_sensors" in device
54
+ and "plugwise_notification" in device["binary_sensors"]
55
+ ):
56
+ data["binary_sensors"]["plugwise_notification"] = bool(self._notifications)
57
+ self._count += 1
58
+
59
+ def _update_for_cooling(self, device: DeviceData) -> None:
60
+ """Helper-function for adding/updating various cooling-related values."""
61
+ # For Anna and heating + cooling, replace setpoint with setpoint_high/_low
62
+ if (
63
+ self.smile(ANNA)
64
+ and self._cooling_present
65
+ and device["dev_class"] == "thermostat"
66
+ ):
67
+ thermostat = device["thermostat"]
68
+ sensors = device["sensors"]
69
+ temp_dict: ActuatorData = {
70
+ "setpoint_low": thermostat["setpoint"],
71
+ "setpoint_high": MAX_SETPOINT,
72
+ }
73
+ if self._cooling_enabled:
74
+ temp_dict = {
75
+ "setpoint_low": MIN_SETPOINT,
76
+ "setpoint_high": thermostat["setpoint"],
77
+ }
78
+ thermostat.pop("setpoint")
79
+ temp_dict.update(thermostat)
80
+ device["thermostat"] = temp_dict
81
+ if "setpoint" in sensors:
82
+ sensors.pop("setpoint")
83
+ sensors["setpoint_low"] = temp_dict["setpoint_low"]
84
+ sensors["setpoint_high"] = temp_dict["setpoint_high"]
85
+ self._count += 2
86
+
87
+ def _all_device_data(self) -> None:
88
+ """Helper-function for get_all_devices().
89
+
90
+ Collect data for each device and add to self.gw_data and self.gw_devices.
91
+ """
92
+ self._update_gw_devices()
93
+ self.device_items = self._count
94
+ self.gw_data.update(
95
+ {
96
+ "gateway_id": self.gateway_id,
97
+ "item_count": self._count,
98
+ "notifications": self._notifications,
99
+ "smile_name": self.smile_name,
100
+ }
101
+ )
102
+ if self._is_thermostat:
103
+ self.gw_data.update(
104
+ {"heater_id": self._heater_id, "cooling_present": self._cooling_present}
105
+ )
106
+
107
+ def _device_data_switching_group(
108
+ self, device: DeviceData, data: DeviceData
109
+ ) -> None:
110
+ """Helper-function for _get_device_data().
111
+
112
+ Determine switching group device data.
113
+ """
114
+ if device["dev_class"] in SWITCH_GROUP_TYPES:
115
+ counter = 0
116
+ for member in device["members"]:
117
+ if self.gw_devices[member]["switches"].get("relay"):
118
+ counter += 1
119
+ data["switches"]["relay"] = counter != 0
120
+ self._count += 1
121
+
122
+ def _device_data_adam(self, device: DeviceData, data: DeviceData) -> None:
123
+ """Helper-function for _get_device_data().
124
+
125
+ Determine Adam heating-status for on-off heating via valves,
126
+ available regulations_modes and thermostat control_states.
127
+ """
128
+ if self.smile(ADAM):
129
+ # Indicate heating_state based on valves being open in case of city-provided heating
130
+ if (
131
+ device["dev_class"] == "heater_central"
132
+ and self._on_off_device
133
+ and isinstance(self._heating_valves(), int)
134
+ ):
135
+ data["binary_sensors"]["heating_state"] = self._heating_valves() != 0
136
+
137
+ # Show the allowed regulation modes and gateway_modes
138
+ if device["dev_class"] == "gateway":
139
+ if self._reg_allowed_modes:
140
+ data["regulation_modes"] = self._reg_allowed_modes
141
+ self._count += 1
142
+ if self._gw_allowed_modes:
143
+ data["gateway_modes"] = self._gw_allowed_modes
144
+ self._count += 1
145
+
146
+ # Control_state, only for Adam master thermostats
147
+ if device["dev_class"] in ZONE_THERMOSTATS:
148
+ loc_id = device["location"]
149
+ if ctrl_state := self._control_state(loc_id):
150
+ data["control_state"] = ctrl_state
151
+ self._count += 1
152
+
153
+ def _device_data_climate(self, device: DeviceData, data: DeviceData) -> None:
154
+ """Helper-function for _get_device_data().
155
+
156
+ Determine climate-control device data.
157
+ """
158
+ loc_id = device["location"]
159
+
160
+ # Presets
161
+ data["preset_modes"] = None
162
+ data["active_preset"] = None
163
+ self._count += 2
164
+ if presets := self._presets(loc_id):
165
+ data["preset_modes"] = list(presets)
166
+ data["active_preset"] = self._preset(loc_id)
167
+
168
+ # Schedule
169
+ avail_schedules, sel_schedule = self._schedules(loc_id)
170
+ data["available_schedules"] = avail_schedules
171
+ data["select_schedule"] = sel_schedule
172
+ self._count += 2
173
+
174
+ # Operation modes: auto, heat, heat_cool, cool and off
175
+ data["mode"] = "auto"
176
+ self._count += 1
177
+ if sel_schedule == NONE:
178
+ data["mode"] = "heat"
179
+ if self._cooling_present:
180
+ data["mode"] = "cool" if self.check_reg_mode("cooling") else "heat_cool"
181
+
182
+ if self.check_reg_mode("off"):
183
+ data["mode"] = "off"
184
+
185
+ if NONE not in avail_schedules:
186
+ self._get_schedule_states_with_off(
187
+ loc_id, avail_schedules, sel_schedule, data
188
+ )
189
+
190
+ def check_reg_mode(self, mode: str) -> bool:
191
+ """Helper-function for device_data_climate()."""
192
+ gateway = self.gw_devices[self.gateway_id]
193
+ return (
194
+ "regulation_modes" in gateway and gateway["select_regulation_mode"] == mode
195
+ )
196
+
197
+ def _get_schedule_states_with_off(
198
+ self, location: str, schedules: list[str], selected: str, data: DeviceData
199
+ ) -> None:
200
+ """Collect schedules with states for each thermostat.
201
+
202
+ Also, replace NONE by OFF when none of the schedules are active.
203
+ """
204
+ loc_schedule_states: dict[str, str] = {}
205
+ for schedule in schedules:
206
+ loc_schedule_states[schedule] = "off"
207
+ if schedule == selected and data["mode"] == "auto":
208
+ loc_schedule_states[schedule] = "on"
209
+ self._schedule_old_states[location] = loc_schedule_states
210
+
211
+ all_off = True
212
+ for state in self._schedule_old_states[location].values():
213
+ if state == "on":
214
+ all_off = False
215
+ if all_off:
216
+ data["select_schedule"] = OFF
217
+
218
+ def _check_availability(
219
+ self, device: DeviceData, dev_class: str, data: DeviceData, message: str
220
+ ) -> None:
221
+ """Helper-function for _get_device_data().
222
+
223
+ Provide availability status for the wired-commected devices.
224
+ """
225
+ if device["dev_class"] == dev_class:
226
+ data["available"] = True
227
+ self._count += 1
228
+ for item in self._notifications.values():
229
+ for msg in item.values():
230
+ if message in msg:
231
+ data["available"] = False
232
+
233
+ def _get_device_data(self, dev_id: str) -> DeviceData:
234
+ """Helper-function for _all_device_data() and async_update().
235
+
236
+ Provide device-data, based on Location ID (= dev_id), from APPLIANCES.
237
+ """
238
+ device = self.gw_devices[dev_id]
239
+ data = self._get_measurement_data(dev_id)
240
+
241
+ # Check availability of wired-connected devices
242
+ # Smartmeter
243
+ self._check_availability(
244
+ device, "smartmeter", data, "P1 does not seem to be connected"
245
+ )
246
+ # OpenTherm device
247
+ if device["name"] != "OnOff":
248
+ self._check_availability(
249
+ device, "heater_central", data, "no OpenTherm communication"
250
+ )
251
+
252
+ # Switching groups data
253
+ self._device_data_switching_group(device, data)
254
+ # Adam data
255
+ self._device_data_adam(device, data)
256
+ # Skip obtaining data for non master-thermostats
257
+ if device["dev_class"] not in ZONE_THERMOSTATS:
258
+ return data
259
+
260
+ # Thermostat data (presets, temperatures etc)
261
+ self._device_data_climate(device, data)
262
+
263
+ return data