plugwise 1.6.2__py3-none-any.whl → 1.6.4__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/helper.py CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  Plugwise Smile protocol helpers.
4
4
  """
5
+
5
6
  from __future__ import annotations
6
7
 
7
8
  import asyncio
@@ -28,6 +29,7 @@ from plugwise.constants import (
28
29
  NONE,
29
30
  OFF,
30
31
  P1_MEASUREMENTS,
32
+ PRIORITY_DEVICE_CLASSES,
31
33
  TEMP_CELSIUS,
32
34
  THERMOSTAT_CLASSES,
33
35
  TOGGLES,
@@ -67,6 +69,15 @@ from munch import Munch
67
69
  from packaging import version
68
70
 
69
71
 
72
+ def search_actuator_functionalities(appliance: etree, actuator: str) -> etree | None:
73
+ """Helper-function for finding the relevant actuator xml-structure."""
74
+ locator = f"./actuator_functionalities/{actuator}"
75
+ if (search := appliance.find(locator)) is not None:
76
+ return search
77
+
78
+ return None
79
+
80
+
70
81
  class SmileComm:
71
82
  """The SmileComm class."""
72
83
 
@@ -179,7 +190,9 @@ class SmileComm:
179
190
  # Command accepted gives empty body with status 202
180
191
  return
181
192
  case 401:
182
- msg = "Invalid Plugwise login, please retry with the correct credentials."
193
+ msg = (
194
+ "Invalid Plugwise login, please retry with the correct credentials."
195
+ )
183
196
  LOGGER.error("%s", msg)
184
197
  raise InvalidAuthentication
185
198
  case 405:
@@ -266,7 +279,11 @@ class SmileHelper(SmileCommon):
266
279
  SmileCommon.__init__(self)
267
280
 
268
281
  def _all_appliances(self) -> None:
269
- """Collect all appliances with relevant info."""
282
+ """Collect all appliances with relevant info.
283
+
284
+ Also, collect the P1 smartmeter info from a location
285
+ as this one is not available as an appliance.
286
+ """
270
287
  self._count = 0
271
288
  self._all_locations()
272
289
 
@@ -318,55 +335,33 @@ class SmileHelper(SmileCommon):
318
335
  if not (appl := self._appliance_info_finder(appl, appliance)):
319
336
  continue
320
337
 
321
- # P1: for gateway and smartmeter switch entity_id - part 1
322
- # This is done to avoid breakage in HA Core
323
- if appl.pwclass == "gateway" and self.smile_type == "power":
324
- appl.entity_id = appl.location
325
-
326
338
  self._create_gw_entities(appl)
327
339
 
328
- # For P1 collect the connected SmartMeter info
329
340
  if self.smile_type == "power":
330
- self._p1_smartmeter_info_finder(appl)
331
- # P1: for gateway and smartmeter switch entity_id - part 2
332
- for item in self.gw_entities:
333
- if item != self.gateway_id:
334
- self.gateway_id = item
335
- # Leave for-loop to avoid a 2nd device_id switch
336
- break
337
-
338
- # Place the gateway and optional heater_central devices as 1st and 2nd
339
- for dev_class in ("heater_central", "gateway"):
340
- for entity_id, entity in dict(self.gw_entities).items():
341
- if entity["dev_class"] == dev_class:
342
- tmp_entity = entity
343
- self.gw_entities.pop(entity_id)
344
- cleared_dict = self.gw_entities
345
- add_to_front = {entity_id: tmp_entity}
346
- self.gw_entities = {**add_to_front, **cleared_dict}
341
+ self._get_p1_smartmeter_info()
347
342
 
348
- def _all_locations(self) -> None:
349
- """Collect all locations."""
350
- loc = Munch()
351
- locations = self._domain_objects.findall("./location")
352
- for location in locations:
353
- loc.name = location.find("name").text
354
- loc.loc_id = location.attrib["id"]
355
- if loc.name == "Home":
356
- self._home_location = loc.loc_id
343
+ # Sort the gw_entities
344
+ self._sort_gw_entities()
357
345
 
358
- self._loc_data[loc.loc_id] = {"name": loc.name}
346
+ def _get_p1_smartmeter_info(self) -> None:
347
+ """For P1 collect the connected SmartMeter info from the Home/building location.
359
348
 
360
- def _p1_smartmeter_info_finder(self, appl: Munch) -> None:
361
- """Collect P1 DSMR SmartMeter info."""
349
+ Note: For P1, the entity_id for the gateway and smartmeter are
350
+ switched to maintain backward compatibility with existing implementations.
351
+ """
352
+ appl = Munch()
362
353
  loc_id = next(iter(self._loc_data.keys()))
363
- location = self._domain_objects.find(f'./location[@id="{loc_id}"]')
354
+ if (
355
+ location := self._domain_objects.find(f'./location[@id="{loc_id}"]')
356
+ ) is None:
357
+ return
358
+
364
359
  locator = MODULE_LOCATOR
365
360
  module_data = self._get_module_data(location, locator)
366
361
  if not module_data["contents"]:
367
362
  LOGGER.error("No module data found for SmartMeter") # pragma: no cover
368
- return None # pragma: no cover
369
-
363
+ return # pragma: no cover
364
+ appl.available = None
370
365
  appl.entity_id = self.gateway_id
371
366
  appl.firmware = module_data["firmware_version"]
372
367
  appl.hardware = module_data["hardware_version"]
@@ -379,21 +374,52 @@ class SmileHelper(SmileCommon):
379
374
  appl.vendor_name = module_data["vendor_name"]
380
375
  appl.zigbee_mac = None
381
376
 
377
+ # Replace the entity_id of the gateway by the smartmeter location_id
378
+ self.gw_entities[loc_id] = self.gw_entities.pop(self.gateway_id)
379
+ self.gateway_id = loc_id
380
+
382
381
  self._create_gw_entities(appl)
383
382
 
383
+ def _sort_gw_entities(self) -> None:
384
+ """Place the gateway and optional heater_central entities as 1st and 2nd."""
385
+ for dev_class in PRIORITY_DEVICE_CLASSES:
386
+ for entity_id, entity in dict(self.gw_entities).items():
387
+ if entity["dev_class"] == dev_class:
388
+ priority_entity = entity
389
+ self.gw_entities.pop(entity_id)
390
+ other_entities = self.gw_entities
391
+ priority_entities = {entity_id: priority_entity}
392
+ self.gw_entities = {**priority_entities, **other_entities}
393
+
394
+ def _all_locations(self) -> None:
395
+ """Collect all locations."""
396
+ loc = Munch()
397
+ locations = self._domain_objects.findall("./location")
398
+ for location in locations:
399
+ loc.name = location.find("name").text
400
+ loc.loc_id = location.attrib["id"]
401
+ if loc.name == "Home":
402
+ self._home_location = loc.loc_id
403
+
404
+ self._loc_data[loc.loc_id] = {"name": loc.name}
405
+
384
406
  def _appliance_info_finder(self, appl: Munch, appliance: etree) -> Munch:
385
407
  """Collect info for all appliances found."""
386
408
  match appl.pwclass:
387
409
  case "gateway":
388
- # Collect gateway device info
410
+ # Collect gateway entity info
389
411
  return self._appl_gateway_info(appl, appliance)
390
412
  case _ as dev_class if dev_class in THERMOSTAT_CLASSES:
391
- # Collect thermostat device info
413
+ # Collect thermostat entity info
392
414
  return self._appl_thermostat_info(appl, appliance)
393
415
  case "heater_central":
394
- # Collect heater_central device info
395
- self._appl_heater_central_info(appl, appliance, False) # False means non-legacy device
396
- self._appl_dhw_mode_info(appl, appliance)
416
+ # Collect heater_central entity info
417
+ self._appl_heater_central_info(
418
+ appl, appliance, False
419
+ ) # False means non-legacy entity
420
+ self._dhw_allowed_modes = self._get_appl_actuator_modes(
421
+ appliance, "domestic_hot_water_mode_control_functionality"
422
+ )
397
423
  # Skip orphaned heater_central (Core Issue #104433)
398
424
  if appl.entity_id != self._heater_id:
399
425
  return Munch()
@@ -430,11 +456,15 @@ class SmileHelper(SmileCommon):
430
456
 
431
457
  # Adam: collect the ZigBee MAC address of the Smile
432
458
  if self.smile(ADAM):
433
- if (found := self._domain_objects.find(".//protocols/zig_bee_coordinator")) is not None:
459
+ if (
460
+ found := self._domain_objects.find(".//protocols/zig_bee_coordinator")
461
+ ) is not None:
434
462
  appl.zigbee_mac = found.find("mac_address").text
435
463
 
436
464
  # Also, collect regulation_modes and check for cooling, indicating cooling-mode is present
437
- self._appl_regulation_mode_info(appliance)
465
+ self._reg_allowed_modes = self._get_appl_actuator_modes(
466
+ appliance, "regulation_mode_control_functionality"
467
+ )
438
468
 
439
469
  # Finally, collect the gateway_modes
440
470
  self._gw_allowed_modes = []
@@ -445,32 +475,24 @@ class SmileHelper(SmileCommon):
445
475
 
446
476
  return appl
447
477
 
448
- def _appl_regulation_mode_info(self, appliance: etree) -> None:
449
- """Helper-function for _appliance_info_finder()."""
450
- reg_mode_list: list[str] = []
451
- locator = "./actuator_functionalities/regulation_mode_control_functionality"
452
- if (search := appliance.find(locator)) is not None:
453
- if search.find("allowed_modes") is not None:
454
- for mode in search.find("allowed_modes"):
455
- reg_mode_list.append(mode.text)
456
- if mode.text == "cooling":
457
- self._cooling_present = True
458
- self._reg_allowed_modes = reg_mode_list
459
-
460
- def _appl_dhw_mode_info(self, appl: Munch, appliance: etree) -> Munch:
461
- """Helper-function for _appliance_info_finder().
462
-
463
- Collect dhw control operation modes - Anna + Loria.
464
- """
465
- dhw_mode_list: list[str] = []
466
- locator = "./actuator_functionalities/domestic_hot_water_mode_control_functionality"
467
- if (search := appliance.find(locator)) is not None:
468
- if search.find("allowed_modes") is not None:
469
- for mode in search.find("allowed_modes"):
470
- dhw_mode_list.append(mode.text)
471
- self._dhw_allowed_modes = dhw_mode_list
478
+ def _get_appl_actuator_modes(
479
+ self, appliance: etree, actuator_type: str
480
+ ) -> list[str]:
481
+ """Get allowed modes for the given actuator type."""
482
+ mode_list: list[str] = []
483
+ if (
484
+ search := search_actuator_functionalities(appliance, actuator_type)
485
+ ) is not None and (modes := search.find("allowed_modes")) is not None:
486
+ for mode in modes:
487
+ mode_list.append(mode.text)
488
+ self._check_cooling_mode(mode.text)
472
489
 
473
- return appl
490
+ return mode_list
491
+
492
+ def _check_cooling_mode(self, mode: str) -> None:
493
+ """Check if cooling mode is present and update state."""
494
+ if mode == "cooling":
495
+ self._cooling_present = True
474
496
 
475
497
  def _get_appliances_with_offset_functionality(self) -> list[str]:
476
498
  """Helper-function collecting all appliance that have offset_functionality."""
@@ -510,7 +532,7 @@ class SmileHelper(SmileCommon):
510
532
  # !! DON'T CHANGE below two if-lines, will break stuff !!
511
533
  if self.smile_type == "power":
512
534
  if entity["dev_class"] == "smartmeter":
513
- data.update(self._power_data_from_location(entity["location"]))
535
+ data.update(self._power_data_from_location(entity["location"]))
514
536
 
515
537
  return data
516
538
 
@@ -636,13 +658,19 @@ class SmileHelper(SmileCommon):
636
658
  def _get_actuator_functionalities(
637
659
  self, xml: etree, entity: GwEntityData, data: GwEntityData
638
660
  ) -> None:
639
- """Helper-function for _get_measurement_data()."""
661
+ """Get and process the actuator_functionalities details for an entity.
662
+
663
+ Add the resulting dict(s) to the entity's data.
664
+ """
640
665
  for item in ACTIVE_ACTUATORS:
641
666
  # Skip max_dhw_temperature, not initially valid,
642
667
  # skip thermostat for all but zones with thermostats
643
668
  if item == "max_dhw_temperature" or (
644
- item == "thermostat" and (
645
- entity["dev_class"] != "climate" if self.smile(ADAM) else entity["dev_class"] != "thermostat"
669
+ item == "thermostat"
670
+ and (
671
+ entity["dev_class"] != "climate"
672
+ if self.smile(ADAM)
673
+ else entity["dev_class"] != "thermostat"
646
674
  )
647
675
  ):
648
676
  continue
@@ -689,6 +717,21 @@ class SmileHelper(SmileCommon):
689
717
  act_item = cast(ActuatorType, item)
690
718
  data[act_item] = temp_dict
691
719
 
720
+ def _get_actuator_mode(
721
+ self, appliance: etree, entity_id: str, key: str
722
+ ) -> str | None:
723
+ """Helper-function for _get_regulation_mode and _get_gateway_mode.
724
+
725
+ Collect the requested gateway mode.
726
+ """
727
+ if not (self.smile(ADAM) and entity_id == self.gateway_id):
728
+ return None
729
+
730
+ if (search := search_actuator_functionalities(appliance, key)) is not None:
731
+ return str(search.find("mode").text)
732
+
733
+ return None
734
+
692
735
  def _get_regulation_mode(
693
736
  self, appliance: etree, entity_id: str, data: GwEntityData
694
737
  ) -> None:
@@ -696,14 +739,14 @@ class SmileHelper(SmileCommon):
696
739
 
697
740
  Adam: collect the gateway regulation_mode.
698
741
  """
699
- if not (self.smile(ADAM) and entity_id == self.gateway_id):
700
- return
701
-
702
- locator = "./actuator_functionalities/regulation_mode_control_functionality"
703
- if (search := appliance.find(locator)) is not None:
704
- data["select_regulation_mode"] = search.find("mode").text
742
+ if (
743
+ mode := self._get_actuator_mode(
744
+ appliance, entity_id, "regulation_mode_control_functionality"
745
+ )
746
+ ) is not None:
747
+ data["select_regulation_mode"] = mode
705
748
  self._count += 1
706
- self._cooling_enabled = data["select_regulation_mode"] == "cooling"
749
+ self._cooling_enabled = mode == "cooling"
707
750
 
708
751
  def _get_gateway_mode(
709
752
  self, appliance: etree, entity_id: str, data: GwEntityData
@@ -712,12 +755,12 @@ class SmileHelper(SmileCommon):
712
755
 
713
756
  Adam: collect the gateway mode.
714
757
  """
715
- if not (self.smile(ADAM) and entity_id == self.gateway_id):
716
- return
717
-
718
- locator = "./actuator_functionalities/gateway_mode_control_functionality"
719
- if (search := appliance.find(locator)) is not None:
720
- data["select_gateway_mode"] = search.find("mode").text
758
+ if (
759
+ mode := self._get_actuator_mode(
760
+ appliance, entity_id, "gateway_mode_control_functionality"
761
+ )
762
+ ) is not None:
763
+ data["select_gateway_mode"] = mode
721
764
  self._count += 1
722
765
 
723
766
  def _get_gateway_outdoor_temp(self, entity_id: str, data: GwEntityData) -> None:
@@ -768,19 +811,23 @@ class SmileHelper(SmileCommon):
768
811
  data["binary_sensors"]["heating_state"] = data["c_heating_state"]
769
812
 
770
813
  if self.smile(ADAM):
814
+ # First count when not present, then create and init to False.
815
+ # When present init to False
771
816
  if "heating_state" not in data["binary_sensors"]:
772
817
  self._count += 1
773
818
  data["binary_sensors"]["heating_state"] = False
819
+
774
820
  if "cooling_state" not in data["binary_sensors"]:
775
821
  self._count += 1
776
822
  data["binary_sensors"]["cooling_state"] = False
823
+
777
824
  if self._cooling_enabled:
778
825
  data["binary_sensors"]["cooling_state"] = data["c_heating_state"]
779
826
  else:
780
827
  data["binary_sensors"]["heating_state"] = data["c_heating_state"]
781
828
 
782
829
  def _update_anna_cooling(self, entity_id: str, data: GwEntityData) -> None:
783
- """Update the Anna heater_central device for cooling.
830
+ """Update the Anna heater_central entity for cooling.
784
831
 
785
832
  Support added for Techneco Elga and Thercon Loria/Thermastage.
786
833
  """
@@ -798,7 +845,11 @@ class SmileHelper(SmileCommon):
798
845
  # Techneco Elga has cooling-capability
799
846
  self._cooling_present = True
800
847
  data["model"] = "Generic heater/cooler"
801
- self._cooling_enabled = data["elga_status_code"] in (8, 9)
848
+ # Cooling_enabled in xml does NOT show the correct status!
849
+ # Setting it specifically:
850
+ self._cooling_enabled = data["binary_sensors"]["cooling_enabled"] = data[
851
+ "elga_status_code"
852
+ ] in (8, 9)
802
853
  data["binary_sensors"]["cooling_state"] = self._cooling_active = (
803
854
  data["elga_status_code"] == 8
804
855
  )
@@ -812,11 +863,17 @@ class SmileHelper(SmileCommon):
812
863
 
813
864
  def _update_loria_cooling(self, data: GwEntityData) -> None:
814
865
  """Loria/Thermastage: base cooling-related on cooling_state and modulation_level."""
815
- self._cooling_enabled = data["binary_sensors"]["cooling_state"]
866
+ # For Loria/Thermastage it's not clear if cooling_enabled in xml shows the correct status,
867
+ # setting it specifically:
868
+ self._cooling_enabled = data["binary_sensors"]["cooling_enabled"] = data[
869
+ "binary_sensors"
870
+ ]["cooling_state"]
816
871
  self._cooling_active = data["sensors"]["modulation_level"] == 100
817
872
  # For Loria the above does not work (pw-beta issue #301)
818
873
  if "cooling_ena_switch" in data["switches"]:
819
- self._cooling_enabled = data["switches"]["cooling_ena_switch"]
874
+ self._cooling_enabled = data["binary_sensors"]["cooling_enabled"] = data[
875
+ "switches"
876
+ ]["cooling_ena_switch"]
820
877
  self._cooling_active = data["binary_sensors"]["cooling_state"]
821
878
 
822
879
  def _cleanup_data(self, data: GwEntityData) -> None:
@@ -866,7 +923,10 @@ class SmileHelper(SmileCommon):
866
923
  "dev_class": "climate",
867
924
  "model": "ThermoZone",
868
925
  "name": loc_data["name"],
869
- "thermostats": {"primary": loc_data["primary"], "secondary": loc_data["secondary"]},
926
+ "thermostats": {
927
+ "primary": loc_data["primary"],
928
+ "secondary": loc_data["secondary"],
929
+ },
870
930
  "vendor": "Plugwise",
871
931
  }
872
932
  self._count += 3
@@ -906,10 +966,12 @@ class SmileHelper(SmileCommon):
906
966
  if thermo_matching[appl_class] == thermo_loc["primary_prio"]:
907
967
  thermo_loc["primary"].append(appliance_id)
908
968
  # Pre-elect new primary
909
- elif (thermo_rank := thermo_matching[appl_class]) > thermo_loc["primary_prio"]:
969
+ elif (thermo_rank := thermo_matching[appl_class]) > thermo_loc[
970
+ "primary_prio"
971
+ ]:
910
972
  thermo_loc["primary_prio"] = thermo_rank
911
973
  # Demote former primary
912
- if (tl_primary := thermo_loc["primary"]):
974
+ if tl_primary := thermo_loc["primary"]:
913
975
  thermo_loc["secondary"] += tl_primary
914
976
  thermo_loc["primary"] = []
915
977
 
@@ -1006,9 +1068,17 @@ class SmileHelper(SmileCommon):
1006
1068
  for rule in self._domain_objects.findall(f'./rule[name="{name}"]'):
1007
1069
  active = rule.find("active").text
1008
1070
  if rule.find(locator) is not None:
1009
- schedule_ids[rule.attrib["id"]] = {"location": loc_id, "name": name, "active": active}
1071
+ schedule_ids[rule.attrib["id"]] = {
1072
+ "location": loc_id,
1073
+ "name": name,
1074
+ "active": active,
1075
+ }
1010
1076
  else:
1011
- schedule_ids[rule.attrib["id"]] = {"location": NONE, "name": name, "active": active}
1077
+ schedule_ids[rule.attrib["id"]] = {
1078
+ "location": NONE,
1079
+ "name": name,
1080
+ "active": active,
1081
+ }
1012
1082
 
1013
1083
  return schedule_ids
1014
1084
 
@@ -1025,9 +1095,17 @@ class SmileHelper(SmileCommon):
1025
1095
  name = rule.find("name").text
1026
1096
  active = rule.find("active").text
1027
1097
  if rule.find(locator2) is not None:
1028
- schedule_ids[rule.attrib["id"]] = {"location": loc_id, "name": name, "active": active}
1098
+ schedule_ids[rule.attrib["id"]] = {
1099
+ "location": loc_id,
1100
+ "name": name,
1101
+ "active": active,
1102
+ }
1029
1103
  else:
1030
- schedule_ids[rule.attrib["id"]] = {"location": NONE, "name": name, "active": active}
1104
+ schedule_ids[rule.attrib["id"]] = {
1105
+ "location": NONE,
1106
+ "name": name,
1107
+ "active": active,
1108
+ }
1031
1109
 
1032
1110
  return schedule_ids
1033
1111
 
plugwise/legacy/data.py CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  Plugwise Smile protocol data-collection helpers for legacy devices.
4
4
  """
5
+
5
6
  from __future__ import annotations
6
7
 
7
8
  # Dict as class
@@ -63,6 +64,7 @@ class SmileLegacyData(SmileLegacyHelper):
63
64
 
64
65
  # Thermostat data (presets, temperatures etc)
65
66
  self._climate_data(entity, data)
67
+ self._get_anna_control_state(data)
66
68
 
67
69
  return data
68
70
 
@@ -91,3 +93,14 @@ class SmileLegacyData(SmileLegacyHelper):
91
93
  self._count += 1
92
94
  if sel_schedule in (NONE, OFF):
93
95
  data["climate_mode"] = "heat"
96
+
97
+ def _get_anna_control_state(self, data: GwEntityData) -> None:
98
+ """Set the thermostat control_state based on the opentherm/onoff device state."""
99
+ data["control_state"] = "idle"
100
+ for entity in self.gw_entities.values():
101
+ if entity["dev_class"] != "heater_central":
102
+ continue
103
+
104
+ binary_sensors = entity["binary_sensors"]
105
+ if binary_sensors["heating_state"]:
106
+ data["control_state"] = "heating"
plugwise/legacy/helper.py CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  Plugwise Smile protocol helpers.
4
4
  """
5
+
5
6
  from __future__ import annotations
6
7
 
7
8
  from typing import cast
@@ -169,10 +170,7 @@ class SmileLegacyHelper(SmileCommon):
169
170
  loc.loc_id = location.attrib["id"]
170
171
  # Filter the valid single location for P1 legacy: services not empty
171
172
  locator = "./services"
172
- if (
173
- self.smile_type == "power"
174
- and len(location.find(locator)) == 0
175
- ):
173
+ if self.smile_type == "power" and len(location.find(locator)) == 0:
176
174
  continue
177
175
 
178
176
  if loc.name == "Home":
@@ -212,15 +210,15 @@ class SmileLegacyHelper(SmileCommon):
212
210
  def _appliance_info_finder(self, appliance: etree, appl: Munch) -> Munch:
213
211
  """Collect entity info (Smile/Stretch, Thermostats, OpenTherm/On-Off): firmware, model and vendor name."""
214
212
  match appl.pwclass:
215
- # Collect thermostat entity info
213
+ # Collect thermostat entity info
216
214
  case _ as dev_class if dev_class in THERMOSTAT_CLASSES:
217
215
  return self._appl_thermostat_info(appl, appliance, self._modules)
218
- # Collect heater_central entity info
216
+ # Collect heater_central entity info
219
217
  case "heater_central":
220
218
  return self._appl_heater_central_info(
221
219
  appl, appliance, True, self._appliances, self._modules
222
220
  ) # True means legacy device
223
- # Collect info from Stretches
221
+ # Collect info from Stretches
224
222
  case _:
225
223
  return self._energy_entity_info_finder(appliance, appl)
226
224
 
@@ -231,7 +229,9 @@ class SmileLegacyHelper(SmileCommon):
231
229
  """
232
230
  if self.smile_type in ("power", "stretch"):
233
231
  locator = "./services/electricity_point_meter"
234
- module_data = self._get_module_data(appliance, locator, self._modules, legacy=True)
232
+ module_data = self._get_module_data(
233
+ appliance, locator, self._modules, legacy=True
234
+ )
235
235
  appl.zigbee_mac = module_data["zigbee_mac_address"]
236
236
  # Filter appliance without zigbee_mac, it's an orphaned device
237
237
  if appl.zigbee_mac is None and self.smile_type != "power":
@@ -362,10 +362,7 @@ class SmileLegacyHelper(SmileCommon):
362
362
  self._count_data_items(data)
363
363
 
364
364
  def _get_actuator_functionalities(
365
- self,
366
- xml: etree,
367
- entity: GwEntityData,
368
- data: GwEntityData
365
+ self, xml: etree, entity: GwEntityData, data: GwEntityData
369
366
  ) -> None:
370
367
  """Helper-function for _get_measurement_data()."""
371
368
  for item in ACTIVE_ACTUATORS:
@@ -458,7 +455,9 @@ class SmileLegacyHelper(SmileCommon):
458
455
  active = result.text == "on"
459
456
 
460
457
  # Show an empty schedule as no schedule found
461
- directives = search.find(f'./rule[@id="{rule_id}"]/directives/when/then') is not None
458
+ directives = (
459
+ search.find(f'./rule[@id="{rule_id}"]/directives/when/then') is not None
460
+ )
462
461
  if directives and name is not None:
463
462
  available = [name, OFF]
464
463
  selected = name if active else OFF
plugwise/legacy/smile.py CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  Plugwise backend module for Home Assistant Core - covering the legacy P1, Anna, and Stretch devices.
4
4
  """
5
+
5
6
  from __future__ import annotations
6
7
 
7
8
  from collections.abc import Awaitable, Callable
@@ -24,7 +25,7 @@ from plugwise.constants import (
24
25
  PlugwiseData,
25
26
  ThermoLoc,
26
27
  )
27
- from plugwise.exceptions import ConnectionFailedError, PlugwiseError
28
+ from plugwise.exceptions import ConnectionFailedError, DataMissingError, PlugwiseError
28
29
  from plugwise.legacy.data import SmileLegacyData
29
30
 
30
31
  import aiohttp
@@ -119,18 +120,30 @@ class SmileLegacyAPI(SmileLegacyData):
119
120
  )
120
121
  self.gw_data: GatewayData = {}
121
122
  self.gw_entities: dict[str, GwEntityData] = {}
122
- await self.full_xml_update()
123
- self.get_all_gateway_entities()
123
+ try:
124
+ await self.full_xml_update()
125
+ self.get_all_gateway_entities()
126
+ # Detect failed data-retrieval
127
+ _ = self.gw_entities[self.gateway_id]["location"]
128
+ except KeyError as err: # pragma: no cover
129
+ raise DataMissingError(
130
+ "No (full) Plugwise legacy data received"
131
+ ) from err
124
132
  # Otherwise perform an incremental update
125
133
  else:
126
- self._domain_objects = await self.request(DOMAIN_OBJECTS)
127
- match self._target_smile:
128
- case "smile_v2":
129
- self._modules = await self.request(MODULES)
130
- case self._target_smile if self._target_smile in REQUIRE_APPLIANCES:
131
- self._appliances = await self.request(APPLIANCES)
132
-
133
- self._update_gw_entities()
134
+ try:
135
+ self._domain_objects = await self.request(DOMAIN_OBJECTS)
136
+ match self._target_smile:
137
+ case "smile_v2":
138
+ self._modules = await self.request(MODULES)
139
+ case self._target_smile if self._target_smile in REQUIRE_APPLIANCES:
140
+ self._appliances = await self.request(APPLIANCES)
141
+
142
+ self._update_gw_entities()
143
+ # Detect failed data-retrieval
144
+ _ = self.gw_entities[self.gateway_id]["location"]
145
+ except KeyError as err: # pragma: no cover
146
+ raise DataMissingError("No legacy Plugwise data received") from err
134
147
 
135
148
  self._previous_day_number = day_number
136
149
  return PlugwiseData(
@@ -138,9 +151,9 @@ class SmileLegacyAPI(SmileLegacyData):
138
151
  gateway=self.gw_data,
139
152
  )
140
153
 
141
- ########################################################################################################
142
- ### API Set and HA Service-related Functions ###
143
- ########################################################################################################
154
+ ########################################################################################################
155
+ ### API Set and HA Service-related Functions ###
156
+ ########################################################################################################
144
157
 
145
158
  async def delete_notification(self) -> None:
146
159
  """Set-function placeholder for legacy devices."""
@@ -181,12 +194,16 @@ class SmileLegacyAPI(SmileLegacyData):
181
194
  async def set_regulation_mode(self, mode: str) -> None:
182
195
  """Set-function placeholder for legacy devices."""
183
196
 
184
- async def set_select(self, key: str, loc_id: str, option: str, state: str | None) -> None:
197
+ async def set_select(
198
+ self, key: str, loc_id: str, option: str, state: str | None
199
+ ) -> None:
185
200
  """Set the thermostat schedule option."""
186
201
  # schedule name corresponds to select option
187
202
  await self.set_schedule_state("dummy", state, option)
188
203
 
189
- async def set_schedule_state(self, _: str, state: str | None, name: str | None) -> None:
204
+ async def set_schedule_state(
205
+ self, _: str, state: str | None, name: str | None
206
+ ) -> None:
190
207
  """Activate/deactivate the Schedule.
191
208
 
192
209
  Determined from - DOMAIN_OBJECTS.
@@ -205,7 +222,9 @@ class SmileLegacyAPI(SmileLegacyData):
205
222
  schedule_rule_id = rule.attrib["id"]
206
223
 
207
224
  if schedule_rule_id is None:
208
- raise PlugwiseError("Plugwise: no schedule with this name available.") # pragma: no cover
225
+ raise PlugwiseError(
226
+ "Plugwise: no schedule with this name available."
227
+ ) # pragma: no cover
209
228
 
210
229
  new_state = "false"
211
230
  if state == "on":