plugwise 0.37.1a2__py3-none-any.whl → 0.37.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
plugwise/legacy/helper.py CHANGED
@@ -4,14 +4,12 @@ Plugwise Smile protocol helpers.
4
4
  """
5
5
  from __future__ import annotations
6
6
 
7
- import datetime as dt
8
7
  from typing import cast
9
8
 
10
9
  from plugwise.common import SmileCommon
11
10
  from plugwise.constants import (
12
11
  ACTIVE_ACTUATORS,
13
12
  ACTUATOR_CLASSES,
14
- ANNA,
15
13
  APPLIANCES,
16
14
  ATTR_NAME,
17
15
  ATTR_UNIT_OF_MEASUREMENT,
@@ -24,12 +22,9 @@ from plugwise.constants import (
24
22
  HEATER_CENTRAL_MEASUREMENTS,
25
23
  LIMITS,
26
24
  NONE,
27
- OBSOLETE_MEASUREMENTS,
28
25
  P1_LEGACY_MEASUREMENTS,
29
26
  SENSORS,
30
- SPECIAL_PLUG_TYPES,
31
27
  SPECIALS,
32
- SWITCH_GROUP_TYPES,
33
28
  SWITCHES,
34
29
  TEMP_CELSIUS,
35
30
  THERMOSTAT_CLASSES,
@@ -46,7 +41,7 @@ from plugwise.constants import (
46
41
  SwitchType,
47
42
  ThermoLoc,
48
43
  )
49
- from plugwise.util import format_measure, power_data_local_format, version_to_model
44
+ from plugwise.util import format_measure, skip_obsolete_measurements, version_to_model
50
45
 
51
46
  # This way of importing aiohttp is because of patch/mocking in testing (aiohttp timeouts)
52
47
  from defusedxml import ElementTree as etree
@@ -97,6 +92,60 @@ class SmileLegacyHelper(SmileCommon):
97
92
  self.smile_zigbee_mac_address: str | None
98
93
  SmileCommon.__init__(self)
99
94
 
95
+ def _all_appliances(self) -> None:
96
+ """Collect all appliances with relevant info."""
97
+ self._count = 0
98
+ self._all_locations()
99
+
100
+ self._create_legacy_gateway()
101
+ # For legacy P1 collect the connected SmartMeter info
102
+ if self.smile_type == "power":
103
+ appl = Munch()
104
+ self._p1_smartmeter_info_finder(appl)
105
+ # Legacy P1 has no more devices
106
+ return
107
+
108
+ for appliance in self._appliances.findall("./appliance"):
109
+ appl = Munch()
110
+ appl.pwclass = appliance.find("type").text
111
+ # Skip thermostats that have this key, should be an orphaned device (Core #81712)
112
+ if (
113
+ appl.pwclass == "thermostat"
114
+ and appliance.find("actuator_functionalities/") is None
115
+ ):
116
+ continue # pragma: no cover
117
+
118
+ appl.location = self._home_location
119
+ appl.dev_id = appliance.attrib["id"]
120
+ appl.name = appliance.find("name").text
121
+ appl.model = appl.pwclass.replace("_", " ").title()
122
+ appl.firmware = None
123
+ appl.hardware = None
124
+ appl.mac = None
125
+ appl.zigbee_mac = None
126
+ appl.vendor_name = None
127
+
128
+ # Determine class for this appliance
129
+ # Skip on heater_central when no active device present or on orphaned stretch devices
130
+ if not (appl := self._appliance_info_finder(appliance, appl)):
131
+ continue
132
+
133
+ # Skip orphaned heater_central (Core Issue #104433)
134
+ if appl.pwclass == "heater_central" and appl.dev_id != self._heater_id:
135
+ continue # pragma: no cover
136
+
137
+ self._create_gw_devices(appl)
138
+
139
+ # Place the gateway and optional heater_central devices as 1st and 2nd
140
+ for dev_class in ("heater_central", "gateway"):
141
+ for dev_id, device in dict(self.gw_devices).items():
142
+ if device["dev_class"] == dev_class:
143
+ tmp_device = device
144
+ self.gw_devices.pop(dev_id)
145
+ cleared_dict = self.gw_devices
146
+ add_to_front = {dev_id: tmp_device}
147
+ self.gw_devices = {**add_to_front, **cleared_dict}
148
+
100
149
  def _all_locations(self) -> None:
101
150
  """Collect all locations."""
102
151
  loc = Munch()
@@ -127,6 +176,44 @@ class SmileLegacyHelper(SmileCommon):
127
176
 
128
177
  self.loc_data[loc.loc_id] = {"name": loc.name}
129
178
 
179
+ def _create_legacy_gateway(self) -> None:
180
+ """Create the (missing) gateway devices for legacy Anna, P1 and Stretch.
181
+
182
+ Use the home_location or FAKE_APPL as device id.
183
+ """
184
+ self.gateway_id = self._home_location
185
+ if self.smile_type == "power":
186
+ self.gateway_id = FAKE_APPL
187
+
188
+ self.gw_devices[self.gateway_id] = {"dev_class": "gateway"}
189
+ self._count += 1
190
+ for key, value in {
191
+ "firmware": self.smile_fw_version,
192
+ "location": self._home_location,
193
+ "mac_address": self.smile_mac_address,
194
+ "model": self.smile_model,
195
+ "name": self.smile_name,
196
+ "zigbee_mac_address": self.smile_zigbee_mac_address,
197
+ "vendor": "Plugwise",
198
+ }.items():
199
+ if value is not None:
200
+ gw_key = cast(ApplianceType, key)
201
+ self.gw_devices[self.gateway_id][gw_key] = value
202
+ self._count += 1
203
+
204
+ def _appliance_info_finder(self, appliance: etree, appl: Munch) -> Munch:
205
+ """Collect device info (Smile/Stretch, Thermostats, OpenTherm/On-Off): firmware, model and vendor name."""
206
+ match appl.pwclass:
207
+ # Collect thermostat device info
208
+ case _ as dev_class if dev_class in THERMOSTAT_CLASSES:
209
+ return self._appl_thermostat_info(appl, appliance, self._modules)
210
+ # Collect heater_central device info
211
+ case "heater_central":
212
+ return self._appl_heater_central_info(appl, appliance, self._appliances, self._modules)
213
+ # Collect info from Stretches
214
+ case _:
215
+ return self._energy_device_info_finder(appliance, appl)
216
+
130
217
  def _energy_device_info_finder(self, appliance: etree, appl: Munch) -> Munch:
131
218
  """Helper-function for _appliance_info_finder().
132
219
 
@@ -154,21 +241,6 @@ class SmileLegacyHelper(SmileCommon):
154
241
 
155
242
  return appl # pragma: no cover
156
243
 
157
- def _appliance_info_finder(self, appliance: etree, appl: Munch) -> Munch:
158
- """Collect device info (Smile/Stretch, Thermostats, OpenTherm/On-Off): firmware, model and vendor name."""
159
- # Collect thermostat device info
160
- if appl.pwclass in THERMOSTAT_CLASSES:
161
- return self._appl_thermostat_info(appl, appliance, self._modules)
162
-
163
- # Collect heater_central device info
164
- if appl.pwclass == "heater_central":
165
- return self._appl_heater_central_info(appl, appliance, self._appliances, self._modules)
166
-
167
- # Collect info from Stretches
168
- appl = self._energy_device_info_finder(appliance, appl)
169
-
170
- return appl
171
-
172
244
  def _p1_smartmeter_info_finder(self, appl: Munch) -> None:
173
245
  """Collect P1 DSMR Smartmeter info."""
174
246
  loc_id = next(iter(self.loc_data.keys()))
@@ -182,130 +254,72 @@ class SmileLegacyHelper(SmileCommon):
182
254
  location = self._locations.find(f'./location[@id="{loc_id}"]')
183
255
  appl = self._energy_device_info_finder(location, appl)
184
256
 
185
- self.gw_devices[appl.dev_id] = {"dev_class": appl.pwclass}
186
- self._count += 1
257
+ self._create_gw_devices(appl)
187
258
 
188
- for key, value in {
189
- "firmware": appl.firmware,
190
- "hardware": appl.hardware,
191
- "location": appl.location,
192
- "mac_address": appl.mac,
193
- "model": appl.model,
194
- "name": appl.name,
195
- "zigbee_mac_address": appl.zigbee_mac,
196
- "vendor": appl.vendor_name,
197
- }.items():
198
- if value is not None or key == "location":
199
- p1_key = cast(ApplianceType, key)
200
- self.gw_devices[appl.dev_id][p1_key] = value
201
- self._count += 1
202
-
203
- def _create_legacy_gateway(self) -> None:
204
- """Create the (missing) gateway devices for legacy Anna, P1 and Stretch.
259
+ def _get_measurement_data(self, dev_id: str) -> DeviceData:
260
+ """Helper-function for smile.py: _get_device_data().
205
261
 
206
- Use the home_location or FAKE_APPL as device id.
262
+ Collect the appliance-data based on device id.
207
263
  """
208
- self.gateway_id = self._home_location
264
+ data: DeviceData = {"binary_sensors": {}, "sensors": {}, "switches": {}}
265
+ # Get P1 smartmeter data from LOCATIONS or MODULES
266
+ device = self.gw_devices[dev_id]
267
+ # !! DON'T CHANGE below two if-lines, will break stuff !!
209
268
  if self.smile_type == "power":
210
- self.gateway_id = FAKE_APPL
269
+ if device["dev_class"] == "smartmeter":
270
+ data.update(self._power_data_from_modules())
211
271
 
212
- self.gw_devices[self.gateway_id] = {"dev_class": "gateway"}
213
- self._count += 1
214
- for key, value in {
215
- "firmware": self.smile_fw_version,
216
- "location": self._home_location,
217
- "mac_address": self.smile_mac_address,
218
- "model": self.smile_model,
219
- "name": self.smile_name,
220
- "zigbee_mac_address": self.smile_zigbee_mac_address,
221
- "vendor": "Plugwise",
222
- }.items():
223
- if value is not None:
224
- gw_key = cast(ApplianceType, key)
225
- self.gw_devices[self.gateway_id][gw_key] = value
226
- self._count += 1
272
+ return data
227
273
 
228
- def _all_appliances(self) -> None:
229
- """Collect all appliances with relevant info."""
230
- self._count = 0
231
- self._all_locations()
274
+ measurements = DEVICE_MEASUREMENTS
275
+ if self._is_thermostat and dev_id == self._heater_id:
276
+ measurements = HEATER_CENTRAL_MEASUREMENTS
232
277
 
233
- self._create_legacy_gateway()
234
- # For legacy P1 collect the connected SmartMeter info
235
- if self.smile_type == "power":
236
- appl = Munch()
237
- self._p1_smartmeter_info_finder(appl)
238
- # Legacy P1 has no more devices
239
- return
278
+ if (
279
+ appliance := self._appliances.find(f'./appliance[@id="{dev_id}"]')
280
+ ) is not None:
281
+ self._appliance_measurements(appliance, data, measurements)
282
+ self._get_lock_state(appliance, data, self._stretch_v2)
240
283
 
241
- for appliance in self._appliances.findall("./appliance"):
242
- appl = Munch()
243
- appl.pwclass = appliance.find("type").text
244
- # Skip thermostats that have this key, should be an orphaned device (Core #81712)
245
- if (
246
- appl.pwclass == "thermostat"
247
- and appliance.find("actuator_functionalities/") is None
248
- ):
249
- continue # pragma: no cover
284
+ if appliance.find("type").text in ACTUATOR_CLASSES:
285
+ self._get_actuator_functionalities(appliance, device, data)
250
286
 
251
- appl.location = self._home_location
252
- appl.dev_id = appliance.attrib["id"]
253
- appl.name = appliance.find("name").text
254
- appl.model = appl.pwclass.replace("_", " ").title()
255
- appl.firmware = None
256
- appl.hardware = None
257
- appl.mac = None
258
- appl.zigbee_mac = None
259
- appl.vendor_name = None
287
+ # Adam & Anna: the Smile outdoor_temperature is present in DOMAIN_OBJECTS and LOCATIONS - under Home
288
+ # The outdoor_temperature present in APPLIANCES is a local sensor connected to the active device
289
+ if self._is_thermostat and dev_id == self.gateway_id:
290
+ outdoor_temperature = self._object_value(
291
+ self._home_location, "outdoor_temperature"
292
+ )
293
+ if outdoor_temperature is not None:
294
+ data.update({"sensors": {"outdoor_temperature": outdoor_temperature}})
295
+ self._count += 1
260
296
 
261
- # Determine class for this appliance
262
- # Skip on heater_central when no active device present or on orphaned stretch devices
263
- if not (appl := self._appliance_info_finder(appliance, appl)):
264
- continue
297
+ if "c_heating_state" in data:
298
+ data.pop("c_heating_state")
299
+ self._count -= 1
265
300
 
266
- # Skip orphaned heater_central (Core Issue #104433)
267
- if appl.pwclass == "heater_central" and appl.dev_id != self._heater_id:
268
- continue # pragma: no cover
301
+ return data
269
302
 
270
- self.gw_devices[appl.dev_id] = {"dev_class": appl.pwclass}
271
- self._count += 1
272
- for key, value in {
273
- "firmware": appl.firmware,
274
- "hardware": appl.hardware,
275
- "location": appl.location,
276
- "mac_address": appl.mac,
277
- "model": appl.model,
278
- "name": appl.name,
279
- "zigbee_mac_address": appl.zigbee_mac,
280
- "vendor": appl.vendor_name,
281
- }.items():
282
- if value is not None or key == "location":
283
- appl_key = cast(ApplianceType, key)
284
- self.gw_devices[appl.dev_id][appl_key] = value
285
- self._count += 1
303
+ def _power_data_from_modules(self) -> DeviceData:
304
+ """Helper-function for smile.py: _get_device_data().
286
305
 
287
- # Place the gateway and optional heater_central devices as 1st and 2nd
288
- for dev_class in ("heater_central", "gateway"):
289
- for dev_id, device in dict(self.gw_devices).items():
290
- if device["dev_class"] == dev_class:
291
- tmp_device = device
292
- self.gw_devices.pop(dev_id)
293
- cleared_dict = self.gw_devices
294
- add_to_front = {dev_id: tmp_device}
295
- self.gw_devices = {**add_to_front, **cleared_dict}
306
+ Collect the power-data from MODULES (P1 legacy only).
307
+ """
308
+ direct_data: DeviceData = {"sensors": {}}
309
+ loc = Munch()
310
+ mod_list: list[str] = ["interval_meter", "cumulative_meter", "point_meter"]
311
+ t_string = "tariff_indicator"
296
312
 
297
- def _presets(self) -> dict[str, list[float]]:
298
- """Helper-function for presets() - collect Presets for a legacy Anna."""
299
- presets: dict[str, list[float]] = {}
300
- for directive in self._domain_objects.findall("rule/directives/when/then"):
301
- if directive is not None and directive.get("icon") is not None:
302
- # Ensure list of heating_setpoint, cooling_setpoint
303
- presets[directive.attrib["icon"]] = [
304
- float(directive.attrib["temperature"]),
305
- 0,
306
- ]
313
+ search = self._modules
314
+ mod_logs = search.findall("./module/services")
315
+ for loc.measurement, loc.attrs in P1_LEGACY_MEASUREMENTS.items():
316
+ loc.meas_list = loc.measurement.split("_")
317
+ for loc.logs in mod_logs:
318
+ for loc.log_type in mod_list:
319
+ self._collect_power_values(direct_data, loc, t_string, legacy=True)
307
320
 
308
- return presets
321
+ self._count += len(direct_data["sensors"])
322
+ return direct_data
309
323
 
310
324
  def _appliance_measurements(
311
325
  self,
@@ -320,20 +334,8 @@ class SmileLegacyHelper(SmileCommon):
320
334
  if measurement == "domestic_hot_water_state":
321
335
  continue
322
336
 
323
- # Skip known obsolete measurements
324
- updated_date_locator = (
325
- f'.//logs/point_log[type="{measurement}"]/updated_date'
326
- )
327
- if (
328
- measurement in OBSOLETE_MEASUREMENTS
329
- and (updated_date_key := appliance.find(updated_date_locator))
330
- is not None
331
- ):
332
- updated_date = updated_date_key.text.split("T")[0]
333
- date_1 = dt.datetime.strptime(updated_date, "%Y-%m-%d")
334
- date_2 = dt.datetime.now()
335
- if int((date_2 - date_1).days) > 7:
336
- continue # pragma: no cover
337
+ if skip_obsolete_measurements(appliance, measurement):
338
+ continue # pragma: no cover
337
339
 
338
340
  if new_name := getattr(attrs, ATTR_NAME, None):
339
341
  measurement = new_name
@@ -406,194 +408,19 @@ class SmileLegacyHelper(SmileCommon):
406
408
  act_item = cast(ActuatorType, item)
407
409
  data[act_item] = temp_dict
408
410
 
409
- def _get_measurement_data(self, dev_id: str) -> DeviceData:
410
- """Helper-function for smile.py: _get_device_data().
411
-
412
- Collect the appliance-data based on device id.
413
- """
414
- data: DeviceData = {"binary_sensors": {}, "sensors": {}, "switches": {}}
415
- # Get P1 smartmeter data from LOCATIONS or MODULES
416
- device = self.gw_devices[dev_id]
417
- # !! DON'T CHANGE below two if-lines, will break stuff !!
418
- if self.smile_type == "power":
419
- if device["dev_class"] == "smartmeter":
420
- data.update(self._power_data_from_modules())
421
-
422
- return data
423
-
424
- measurements = DEVICE_MEASUREMENTS
425
- if self._is_thermostat and dev_id == self._heater_id:
426
- measurements = HEATER_CENTRAL_MEASUREMENTS
427
-
428
- if (
429
- appliance := self._appliances.find(f'./appliance[@id="{dev_id}"]')
430
- ) is not None:
431
- self._appliance_measurements(appliance, data, measurements)
432
- self._get_lock_state(appliance, data)
433
-
434
- if appliance.find("type").text in ACTUATOR_CLASSES:
435
- self._get_actuator_functionalities(appliance, device, data)
436
-
437
- # Adam & Anna: the Smile outdoor_temperature is present in DOMAIN_OBJECTS and LOCATIONS - under Home
438
- # The outdoor_temperature present in APPLIANCES is a local sensor connected to the active device
439
- if self._is_thermostat and dev_id == self.gateway_id:
440
- outdoor_temperature = self._object_value(
441
- self._home_location, "outdoor_temperature"
442
- )
443
- if outdoor_temperature is not None:
444
- data.update({"sensors": {"outdoor_temperature": outdoor_temperature}})
445
- self._count += 1
446
-
447
- if "c_heating_state" in data:
448
- data.pop("c_heating_state")
449
- self._count -= 1
450
-
451
- return data
452
-
453
- def _thermostat_uri(self) -> str:
454
- """Determine the location-set_temperature uri - from APPLIANCES."""
455
- locator = "./appliance[type='thermostat']"
456
- appliance_id = self._appliances.find(locator).attrib["id"]
457
-
458
- return f"{APPLIANCES};id={appliance_id}/thermostat"
459
-
460
- def _get_group_switches(self) -> dict[str, DeviceData]:
461
- """Helper-function for smile.py: get_all_devices().
462
-
463
- Collect switching- or pump-group info.
464
- """
465
- switch_groups: dict[str, DeviceData] = {}
466
- # P1 and Anna don't have switchgroups
467
- if self.smile_type == "power" or self.smile(ANNA):
468
- return switch_groups
469
-
470
- for group in self._domain_objects.findall("./group"):
471
- members: list[str] = []
472
- group_id = group.attrib["id"]
473
- group_name = group.find("name").text
474
- group_type = group.find("type").text
475
- group_appliances = group.findall("appliances/appliance")
476
- for item in group_appliances:
477
- # Check if members are not orphaned - stretch
478
- if item.attrib["id"] in self.gw_devices:
479
- members.append(item.attrib["id"])
480
-
481
- if group_type in SWITCH_GROUP_TYPES and members:
482
- switch_groups.update(
483
- {
484
- group_id: {
485
- "dev_class": group_type,
486
- "model": "Switchgroup",
487
- "name": group_name,
488
- "members": members,
489
- },
490
- },
491
- )
492
- self._count += 4
493
-
494
- return switch_groups
495
-
496
- def power_data_energy_diff(
497
- self,
498
- measurement: str,
499
- net_string: SensorType,
500
- f_val: float | int,
501
- direct_data: DeviceData,
502
- ) -> DeviceData:
503
- """Calculate differential energy."""
504
- if (
505
- "electricity" in measurement
506
- and "phase" not in measurement
507
- and "interval" not in net_string
508
- ):
509
- diff = 1
510
- if "produced" in measurement:
511
- diff = -1
512
- if net_string not in direct_data["sensors"]:
513
- tmp_val: float | int = 0
514
- else:
515
- tmp_val = direct_data["sensors"][net_string]
516
-
517
- if isinstance(f_val, int):
518
- tmp_val += f_val * diff
519
- else:
520
- tmp_val += float(f_val * diff)
521
- tmp_val = float(f"{round(tmp_val, 3):.3f}")
522
-
523
- direct_data["sensors"][net_string] = tmp_val
524
-
525
- return direct_data
526
-
527
- def _power_data_peak_value(self, loc: Munch) -> Munch:
528
- """Helper-function for _power_data_from_location() and _power_data_from_modules()."""
529
- loc.found = True
530
- # If locator not found for P1 legacy electricity_point_meter or gas_*_meter data
531
- if loc.logs.find(loc.locator) is None:
532
- if "meter" in loc.log_type and (
533
- "point" in loc.log_type or "gas" in loc.measurement
534
- ):
535
- # Avoid double processing by skipping one peak-list option
536
- if loc.peak_select == "nl_offpeak":
537
- loc.found = False
538
- return loc
539
-
540
- loc.locator = (
541
- f"./{loc.meas_list[0]}_{loc.log_type}/"
542
- f'measurement[@directionality="{loc.meas_list[1]}"]'
543
- )
544
- if loc.logs.find(loc.locator) is None:
545
- loc.found = False
546
- return loc
547
- else:
548
- loc.found = False
549
- return loc
550
-
551
- if (peak := loc.peak_select.split("_")[1]) == "offpeak":
552
- peak = "off_peak"
553
- log_found = loc.log_type.split("_")[0]
554
- loc.key_string = f"{loc.measurement}_{peak}_{log_found}"
555
- if "gas" in loc.measurement or loc.log_type == "point_meter":
556
- loc.key_string = f"{loc.measurement}_{log_found}"
557
- loc.net_string = f"net_electricity_{log_found}"
558
- val = loc.logs.find(loc.locator).text
559
- loc.f_val = power_data_local_format(loc.attrs, loc.key_string, val)
560
-
561
- return loc
562
-
563
- def _power_data_from_modules(self) -> DeviceData:
564
- """Helper-function for smile.py: _get_device_data().
411
+ def _object_value(self, obj_id: str, measurement: str) -> float | int | None:
412
+ """Helper-function for smile.py: _get_device_data() and _device_data_anna().
565
413
 
566
- Collect the power-data from MODULES (P1 legacy only).
414
+ Obtain the value/state for the given object from a location in DOMAIN_OBJECTS
567
415
  """
568
- direct_data: DeviceData = {"sensors": {}}
569
- loc = Munch()
570
- mod_list: list[str] = ["interval_meter", "cumulative_meter", "point_meter"]
571
- peak_list: list[str] = ["nl_peak", "nl_offpeak"]
572
- t_string = "tariff_indicator"
573
-
574
- search = self._modules
575
- mod_logs = search.findall("./module/services")
576
- for loc.measurement, loc.attrs in P1_LEGACY_MEASUREMENTS.items():
577
- loc.meas_list = loc.measurement.split("_")
578
- for loc.logs in mod_logs:
579
- for loc.log_type in mod_list:
580
- for loc.peak_select in peak_list:
581
- loc.locator = (
582
- f"./{loc.meas_list[0]}_{loc.log_type}/measurement"
583
- f'[@directionality="{loc.meas_list[1]}"][@{t_string}="{loc.peak_select}"]'
584
- )
585
- loc = self._power_data_peak_value(loc)
586
- if not loc.found:
587
- continue
588
-
589
- direct_data = self.power_data_energy_diff(
590
- loc.measurement, loc.net_string, loc.f_val, direct_data
591
- )
592
- key = cast(SensorType, loc.key_string)
593
- direct_data["sensors"][key] = loc.f_val
416
+ val: float | int | None = None
417
+ search = self._domain_objects
418
+ locator = f'./location[@id="{obj_id}"]/logs/point_log[type="{measurement}"]/period/measurement'
419
+ if (found := search.find(locator)) is not None:
420
+ val = format_measure(found.text, NONE)
421
+ return val
594
422
 
595
- self._count += len(direct_data["sensors"])
596
- return direct_data
423
+ return val
597
424
 
598
425
  def _preset(self) -> str | None:
599
426
  """Helper-function for smile.py: device_data_climate().
@@ -609,6 +436,19 @@ class SmileLegacyHelper(SmileCommon):
609
436
 
610
437
  return active_rule["icon"]
611
438
 
439
+ def _presets(self) -> dict[str, list[float]]:
440
+ """Helper-function for presets() - collect Presets for a legacy Anna."""
441
+ presets: dict[str, list[float]] = {}
442
+ for directive in self._domain_objects.findall("rule/directives/when/then"):
443
+ if directive is not None and directive.get("icon") is not None:
444
+ # Ensure list of heating_setpoint, cooling_setpoint
445
+ presets[directive.attrib["icon"]] = [
446
+ float(directive.attrib["temperature"]),
447
+ 0,
448
+ ]
449
+
450
+ return presets
451
+
612
452
  def _schedules(self) -> tuple[list[str], str]:
613
453
  """Collect available schedules/schedules for the legacy thermostat."""
614
454
  available: list[str] = [NONE]
@@ -634,32 +474,9 @@ class SmileLegacyHelper(SmileCommon):
634
474
 
635
475
  return available, selected
636
476
 
637
- def _object_value(self, obj_id: str, measurement: str) -> float | int | None:
638
- """Helper-function for smile.py: _get_device_data() and _device_data_anna().
639
-
640
- Obtain the value/state for the given object from a location in DOMAIN_OBJECTS
641
- """
642
- val: float | int | None = None
643
- search = self._domain_objects
644
- locator = f'./location[@id="{obj_id}"]/logs/point_log[type="{measurement}"]/period/measurement'
645
- if (found := search.find(locator)) is not None:
646
- val = format_measure(found.text, NONE)
647
- return val
648
-
649
- return val
650
-
651
- def _get_lock_state(self, xml: etree, data: DeviceData) -> None:
652
- """Helper-function for _get_measurement_data().
477
+ def _thermostat_uri(self) -> str:
478
+ """Determine the location-set_temperature uri - from APPLIANCES."""
479
+ locator = "./appliance[type='thermostat']"
480
+ appliance_id = self._appliances.find(locator).attrib["id"]
653
481
 
654
- Adam & Stretches: obtain the relay-switch lock state.
655
- """
656
- actuator = "actuator_functionalities"
657
- func_type = "relay_functionality"
658
- if self._stretch_v2:
659
- actuator = "actuators"
660
- func_type = "relay"
661
- if xml.find("type").text not in SPECIAL_PLUG_TYPES:
662
- locator = f"./{actuator}/{func_type}/lock"
663
- if (found := xml.find(locator)) is not None:
664
- data["switches"]["lock"] = found.text == "true"
665
- self._count += 1
482
+ return f"{APPLIANCES};id={appliance_id}/thermostat"