plugwise 1.6.3__py3-none-any.whl → 1.7.0__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/constants.py CHANGED
@@ -1,8 +1,8 @@
1
1
  """Plugwise Smile constants."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  from collections import namedtuple
5
- from dataclasses import dataclass
6
6
  import logging
7
7
  from typing import Final, Literal, TypedDict, get_args
8
8
 
@@ -84,6 +84,7 @@ MIN_SETPOINT: Final[float] = 4.0
84
84
  MODULE_LOCATOR: Final = "./logs/point_log/*[@id]"
85
85
  NONE: Final = "None"
86
86
  OFF: Final = "off"
87
+ PRIORITY_DEVICE_CLASSES = ("heater_central", "gateway")
87
88
 
88
89
  # XML data paths
89
90
  APPLIANCES: Final = "/core/appliances"
@@ -167,9 +168,7 @@ HEATER_CENTRAL_MEASUREMENTS: Final[dict[str, DATA | UOM]] = {
167
168
  "intended_boiler_state": DATA(
168
169
  "heating_state", NONE
169
170
  ), # Legacy Anna: shows when heating is active, we don't show dhw_state, cannot be determined reliably
170
- "flame_state": UOM(
171
- NONE
172
- ), # Also present when there is a single gas-heater
171
+ "flame_state": UOM(NONE), # Also present when there is a single gas-heater
173
172
  "intended_boiler_temperature": UOM(
174
173
  TEMP_CELSIUS
175
174
  ), # Non-zero when heating, zero when dhw-heating
@@ -394,14 +393,13 @@ ZONE_THERMOSTATS: Final[tuple[str, ...]] = (
394
393
  )
395
394
 
396
395
 
397
- class GatewayData(TypedDict, total=False):
398
- """The Gateway Data class."""
396
+ class SmileProps(TypedDict, total=False):
397
+ """The SmileProps Data class."""
399
398
 
400
399
  cooling_present: bool
401
400
  gateway_id: str
402
401
  heater_id: str
403
402
  item_count: int
404
- notifications: dict[str, dict[str, str]]
405
403
  reboot: bool
406
404
  smile_name: str
407
405
 
@@ -552,6 +550,7 @@ class GwEntityData(TypedDict, total=False):
552
550
 
553
551
  # Gateway
554
552
  gateway_modes: list[str]
553
+ notifications: dict[str, dict[str, str]]
555
554
  regulation_modes: list[str]
556
555
  select_gateway_mode: str
557
556
  select_regulation_mode: str
@@ -577,12 +576,3 @@ class GwEntityData(TypedDict, total=False):
577
576
  switches: SmileSwitches
578
577
  temperature_offset: ActuatorData
579
578
  thermostat: ActuatorData
580
-
581
-
582
- @dataclass
583
- class PlugwiseData:
584
- """Plugwise data provided as output."""
585
-
586
- devices: dict[str, GwEntityData]
587
- gateway: GatewayData
588
-
plugwise/data.py CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  Plugwise Smile protocol data-collection helpers.
4
4
  """
5
+
5
6
  from __future__ import annotations
6
7
 
7
8
  import re
@@ -15,6 +16,7 @@ from plugwise.constants import (
15
16
  OFF,
16
17
  ActuatorData,
17
18
  GwEntityData,
19
+ SmileProps,
18
20
  )
19
21
  from plugwise.helper import SmileHelper
20
22
  from plugwise.util import remove_empty_platform_dicts
@@ -25,32 +27,27 @@ class SmileData(SmileHelper):
25
27
 
26
28
  def __init__(self) -> None:
27
29
  """Init."""
30
+ self._smile_props: SmileProps
31
+ self._zones: dict[str, GwEntityData] = {}
28
32
  SmileHelper.__init__(self)
29
33
 
30
-
31
34
  def _all_entity_data(self) -> None:
32
35
  """Helper-function for get_all_gateway_entities().
33
36
 
34
- Collect data for each entity and add to self.gw_data and self.gw_entities.
37
+ Collect data for each entity and add to self._smile_props and self.gw_entities.
35
38
  """
36
39
  self._update_gw_entities()
37
40
  if self.smile(ADAM):
38
41
  self._update_zones()
39
42
  self.gw_entities.update(self._zones)
40
43
 
41
- self.gw_data.update(
42
- {
43
- "gateway_id": self.gateway_id,
44
- "item_count": self._count,
45
- "notifications": self._notifications,
46
- "reboot": True,
47
- "smile_name": self.smile_name,
48
- }
49
- )
44
+ self._smile_props["gateway_id"] = self._gateway_id
45
+ self._smile_props["item_count"] = self._count
46
+ self._smile_props["reboot"] = True
47
+ self._smile_props["smile_name"] = self.smile_name
50
48
  if self._is_thermostat:
51
- self.gw_data.update(
52
- {"heater_id": self._heater_id, "cooling_present": self._cooling_present}
53
- )
49
+ self._smile_props["heater_id"] = self._heater_id
50
+ self._smile_props["cooling_present"] = self._cooling_present
54
51
 
55
52
  def _update_zones(self) -> None:
56
53
  """Helper-function for _all_entity_data() and async_update().
@@ -69,7 +66,7 @@ class SmileData(SmileHelper):
69
66
  mac_list: list[str] = []
70
67
  for entity_id, entity in self.gw_entities.items():
71
68
  data = self._get_entity_data(entity_id)
72
- if entity_id == self.gateway_id:
69
+ if entity_id == self._gateway_id:
73
70
  mac_list = self._detect_low_batteries()
74
71
  self._add_or_update_notifications(entity_id, entity, data)
75
72
 
@@ -78,7 +75,13 @@ class SmileData(SmileHelper):
78
75
  mac_list
79
76
  and "low_battery" in entity["binary_sensors"]
80
77
  and entity["zigbee_mac_address"] in mac_list
81
- and entity["dev_class"] in ("thermo_sensor", "thermostatic_radiator_valve", "zone_thermometer", "zone_thermostat")
78
+ and entity["dev_class"]
79
+ in (
80
+ "thermo_sensor",
81
+ "thermostatic_radiator_valve",
82
+ "zone_thermometer",
83
+ "zone_thermostat",
84
+ )
82
85
  )
83
86
  if is_battery_low:
84
87
  entity["binary_sensors"]["low_battery"] = True
@@ -98,7 +101,11 @@ class SmileData(SmileHelper):
98
101
  message: str | None = notification.get("message")
99
102
  warning: str | None = notification.get("warning")
100
103
  notify = message or warning
101
- if notify is not None and all(x in notify for x in matches) and (mac_addresses := mac_pattern.findall(notify)):
104
+ if (
105
+ notify is not None
106
+ and all(x in notify for x in matches)
107
+ and (mac_addresses := mac_pattern.findall(notify))
108
+ ):
102
109
  mac_address = mac_addresses[0] # re.findall() outputs a list
103
110
 
104
111
  if mac_address is not None:
@@ -113,16 +120,15 @@ class SmileData(SmileHelper):
113
120
  ) -> None:
114
121
  """Helper-function adding or updating the Plugwise notifications."""
115
122
  if (
116
- entity_id == self.gateway_id
117
- and (
118
- self._is_thermostat or self.smile_type == "power"
119
- )
123
+ entity_id == self._gateway_id
124
+ and (self._is_thermostat or self.smile_type == "power")
120
125
  ) or (
121
126
  "binary_sensors" in entity
122
127
  and "plugwise_notification" in entity["binary_sensors"]
123
128
  ):
124
129
  data["binary_sensors"]["plugwise_notification"] = bool(self._notifications)
125
- self._count += 1
130
+ data["notifications"] = self._notifications
131
+ self._count += 2
126
132
 
127
133
  def _update_for_cooling(self, entity: GwEntityData) -> None:
128
134
  """Helper-function for adding/updating various cooling-related values."""
@@ -152,7 +158,6 @@ class SmileData(SmileHelper):
152
158
  sensors["setpoint_high"] = temp_dict["setpoint_high"]
153
159
  self._count += 2 # add 4, remove 2
154
160
 
155
-
156
161
  def _get_location_data(self, loc_id: str) -> GwEntityData:
157
162
  """Helper-function for _all_entity_data() and async_update().
158
163
 
@@ -160,13 +165,14 @@ class SmileData(SmileHelper):
160
165
  """
161
166
  zone = self._zones[loc_id]
162
167
  data = self._get_zone_data(loc_id)
163
- if ctrl_state := self._control_state(data, loc_id):
164
- if str(ctrl_state) in ("cooling", "heating", "preheating"):
165
- data["control_state"] = str(ctrl_state)
166
- self._count += 1
167
- if str(ctrl_state) == "off":
168
- data["control_state"] = "idle"
169
- self._count += 1
168
+ data["control_state"] = "idle"
169
+ self._count += 1
170
+ if (ctrl_state := self._control_state(data, loc_id)) and str(ctrl_state) in (
171
+ "cooling",
172
+ "heating",
173
+ "preheating",
174
+ ):
175
+ data["control_state"] = str(ctrl_state)
170
176
 
171
177
  data["sensors"].pop("setpoint") # remove, only used in _control_state()
172
178
  self._count -= 1
@@ -204,6 +210,7 @@ class SmileData(SmileHelper):
204
210
  # Thermostat data for Anna (presets, temperatures etc)
205
211
  if self.smile(ANNA) and entity["dev_class"] == "thermostat":
206
212
  self._climate_data(entity_id, entity, data)
213
+ self._get_anna_control_state(data)
207
214
 
208
215
  return data
209
216
 
@@ -235,7 +242,10 @@ class SmileData(SmileHelper):
235
242
  data["binary_sensors"]["heating_state"] = self._heating_valves() != 0
236
243
  # Add cooling_enabled binary_sensor
237
244
  if "binary_sensors" in data:
238
- if "cooling_enabled" not in data["binary_sensors"] and self._cooling_present:
245
+ if (
246
+ "cooling_enabled" not in data["binary_sensors"]
247
+ and self._cooling_present
248
+ ):
239
249
  data["binary_sensors"]["cooling_enabled"] = self._cooling_enabled
240
250
 
241
251
  # Show the allowed regulation_modes and gateway_modes
@@ -248,10 +258,7 @@ class SmileData(SmileHelper):
248
258
  self._count += 1
249
259
 
250
260
  def _climate_data(
251
- self,
252
- location_id: str,
253
- entity: GwEntityData,
254
- data: GwEntityData
261
+ self, location_id: str, entity: GwEntityData, data: GwEntityData
255
262
  ) -> None:
256
263
  """Helper-function for _get_entity_data().
257
264
 
@@ -282,7 +289,9 @@ class SmileData(SmileHelper):
282
289
  if sel_schedule in (NONE, OFF):
283
290
  data["climate_mode"] = "heat"
284
291
  if self._cooling_present:
285
- data["climate_mode"] = "cool" if self.check_reg_mode("cooling") else "heat_cool"
292
+ data["climate_mode"] = (
293
+ "cool" if self.check_reg_mode("cooling") else "heat_cool"
294
+ )
286
295
 
287
296
  if self.check_reg_mode("off"):
288
297
  data["climate_mode"] = "off"
@@ -294,11 +303,25 @@ class SmileData(SmileHelper):
294
303
 
295
304
  def check_reg_mode(self, mode: str) -> bool:
296
305
  """Helper-function for device_data_climate()."""
297
- gateway = self.gw_entities[self.gateway_id]
306
+ gateway = self.gw_entities[self._gateway_id]
298
307
  return (
299
308
  "regulation_modes" in gateway and gateway["select_regulation_mode"] == mode
300
309
  )
301
310
 
311
+ def _get_anna_control_state(self, data: GwEntityData) -> None:
312
+ """Set the thermostat control_state based on the opentherm/onoff device state."""
313
+ data["control_state"] = "idle"
314
+ self._count += 1
315
+ for entity in self.gw_entities.values():
316
+ if entity["dev_class"] != "heater_central":
317
+ continue
318
+
319
+ binary_sensors = entity["binary_sensors"]
320
+ if binary_sensors["heating_state"]:
321
+ data["control_state"] = "heating"
322
+ if binary_sensors.get("cooling_state"):
323
+ data["control_state"] = "cooling"
324
+
302
325
  def _get_schedule_states_with_off(
303
326
  self, location: str, schedules: list[str], selected: str, data: GwEntityData
304
327
  ) -> None: