plugwise 1.6.4__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/__init__.py CHANGED
@@ -17,7 +17,8 @@ from plugwise.constants import (
17
17
  SMILES,
18
18
  STATUS,
19
19
  SYSTEM,
20
- PlugwiseData,
20
+ GwEntityData,
21
+ SmileProps,
21
22
  ThermoLoc,
22
23
  )
23
24
  from plugwise.exceptions import (
@@ -28,9 +29,9 @@ from plugwise.exceptions import (
28
29
  ResponseError,
29
30
  UnsupportedDeviceError,
30
31
  )
31
- from plugwise.helper import SmileComm
32
32
  from plugwise.legacy.smile import SmileLegacyAPI
33
33
  from plugwise.smile import SmileAPI
34
+ from plugwise.smilecomm import SmileComm
34
35
 
35
36
  import aiohttp
36
37
  from defusedxml import ElementTree as etree
@@ -38,9 +39,7 @@ from packaging.version import Version, parse
38
39
 
39
40
 
40
41
  class Smile(SmileComm):
41
- """The Plugwise SmileConnect class."""
42
-
43
- # pylint: disable=too-many-instance-attributes, too-many-public-methods
42
+ """The main Plugwise Smile API class."""
44
43
 
45
44
  def __init__(
46
45
  self,
@@ -51,19 +50,14 @@ class Smile(SmileComm):
51
50
  username: str = DEFAULT_USERNAME,
52
51
  ) -> None:
53
52
  """Set the constructor for this class."""
54
- self._host = host
55
- self._password = password
56
- self._port = port
57
53
  self._timeout = DEFAULT_LEGACY_TIMEOUT
58
- self._username = username
59
- self._websession = websession
60
54
  super().__init__(
61
- self._host,
62
- self._password,
63
- self._port,
55
+ host,
56
+ password,
57
+ port,
64
58
  self._timeout,
65
- self._username,
66
- self._websession,
59
+ username,
60
+ websession,
67
61
  )
68
62
 
69
63
  self._cooling_present = False
@@ -75,10 +69,9 @@ class Smile(SmileComm):
75
69
  self._opentherm_device = False
76
70
  self._schedule_old_states: dict[str, dict[str, str]] = {}
77
71
  self._smile_api: SmileAPI | SmileLegacyAPI
72
+ self._smile_props: SmileProps = {}
78
73
  self._stretch_v2 = False
79
74
  self._target_smile: str = NONE
80
- self.gateway_id: str = NONE
81
- self.smile_fw_version: Version | None = None
82
75
  self.smile_hostname: str = NONE
83
76
  self.smile_hw_version: str | None = None
84
77
  self.smile_legacy = False
@@ -90,8 +83,40 @@ class Smile(SmileComm):
90
83
  self.smile_version: Version | None = None
91
84
  self.smile_zigbee_mac_address: str | None = None
92
85
 
86
+ @property
87
+ def cooling_present(self) -> bool:
88
+ """Return the cooling capability."""
89
+ if "cooling_present" in self._smile_props:
90
+ return self._smile_props["cooling_present"]
91
+ return False
92
+
93
+ @property
94
+ def gateway_id(self) -> str:
95
+ """Return the gateway-id."""
96
+ return self._smile_props["gateway_id"]
97
+
98
+ @property
99
+ def heater_id(self) -> str:
100
+ """Return the heater-id."""
101
+ if "heater_id" in self._smile_props:
102
+ return self._smile_props["heater_id"]
103
+ return NONE
104
+
105
+ @property
106
+ def item_count(self) -> int:
107
+ """Return the item-count."""
108
+ return self._smile_props["item_count"]
109
+
110
+ @property
111
+ def reboot(self) -> bool:
112
+ """Return the reboot capability.
113
+
114
+ All non-legacy devices support gateway-rebooting.
115
+ """
116
+ return not self.smile_legacy
117
+
93
118
  async def connect(self) -> Version | None:
94
- """Connect to Plugwise device and determine its name, type and version."""
119
+ """Connect to the Plugwise Gateway and determine its name, type, version, and other data."""
95
120
  result = await self._request(DOMAIN_OBJECTS)
96
121
  # Work-around for Stretch fw 2.7.18
97
122
  if not (vendor_names := result.findall("./module/vendor_name")):
@@ -128,10 +153,6 @@ class Smile(SmileComm):
128
153
 
129
154
  self._smile_api = (
130
155
  SmileAPI(
131
- self._host,
132
- self._password,
133
- self._request,
134
- self._websession,
135
156
  self._cooling_present,
136
157
  self._elga,
137
158
  self._is_thermostat,
@@ -139,9 +160,9 @@ class Smile(SmileComm):
139
160
  self._loc_data,
140
161
  self._on_off_device,
141
162
  self._opentherm_device,
163
+ self._request,
142
164
  self._schedule_old_states,
143
- self.gateway_id,
144
- self.smile_fw_version,
165
+ self._smile_props,
145
166
  self.smile_hostname,
146
167
  self.smile_hw_version,
147
168
  self.smile_mac_address,
@@ -150,31 +171,25 @@ class Smile(SmileComm):
150
171
  self.smile_name,
151
172
  self.smile_type,
152
173
  self.smile_version,
153
- self._port,
154
- self._username,
155
174
  )
156
175
  if not self.smile_legacy
157
176
  else SmileLegacyAPI(
158
- self._host,
159
- self._password,
160
- self._request,
161
- self._websession,
162
177
  self._is_thermostat,
163
178
  self._loc_data,
164
179
  self._on_off_device,
165
180
  self._opentherm_device,
181
+ self._request,
182
+ self._smile_props,
166
183
  self._stretch_v2,
167
184
  self._target_smile,
168
- self.smile_fw_version,
169
185
  self.smile_hostname,
170
186
  self.smile_hw_version,
171
187
  self.smile_mac_address,
172
188
  self.smile_model,
173
189
  self.smile_name,
174
190
  self.smile_type,
191
+ self.smile_version,
175
192
  self.smile_zigbee_mac_address,
176
- self._port,
177
- self._username,
178
193
  )
179
194
  )
180
195
 
@@ -186,13 +201,13 @@ class Smile(SmileComm):
186
201
  async def _smile_detect(self, result: etree, dsmrmain: etree) -> None:
187
202
  """Helper-function for connect().
188
203
 
189
- Detect which type of Smile is connected.
204
+ Detect which type of Plugwise Gateway is being connected.
190
205
  """
191
206
  model: str = "Unknown"
192
207
  if (gateway := result.find("./gateway")) is not None:
193
208
  if (v_model := gateway.find("vendor_model")) is not None:
194
209
  model = v_model.text
195
- self.smile_fw_version = parse(gateway.find("firmware_version").text)
210
+ self.smile_version = parse(gateway.find("firmware_version").text)
196
211
  self.smile_hw_version = gateway.find("hardware_version").text
197
212
  self.smile_hostname = gateway.find("hostname").text
198
213
  self.smile_mac_address = gateway.find("mac_address").text
@@ -200,7 +215,7 @@ class Smile(SmileComm):
200
215
  else:
201
216
  model = await self._smile_detect_legacy(result, dsmrmain, model)
202
217
 
203
- if model == "Unknown" or self.smile_fw_version is None: # pragma: no cover
218
+ if model == "Unknown" or self.smile_version is None: # pragma: no cover
204
219
  # Corner case check
205
220
  LOGGER.error(
206
221
  "Unable to find model or version information, please create"
@@ -208,7 +223,7 @@ class Smile(SmileComm):
208
223
  )
209
224
  raise UnsupportedDeviceError
210
225
 
211
- version_major = str(self.smile_fw_version.major)
226
+ version_major = str(self.smile_version.major)
212
227
  self._target_smile = f"{model}_v{version_major}"
213
228
  LOGGER.debug("Plugwise identified as %s", self._target_smile)
214
229
  if self._target_smile not in SMILES:
@@ -232,7 +247,6 @@ class Smile(SmileComm):
232
247
  self.smile_model = "Gateway"
233
248
  self.smile_name = SMILES[self._target_smile].smile_name
234
249
  self.smile_type = SMILES[self._target_smile].smile_type
235
- self.smile_version = self.smile_fw_version
236
250
 
237
251
  if self.smile_type == "stretch":
238
252
  self._stretch_v2 = int(version_major) == 2
@@ -260,7 +274,10 @@ class Smile(SmileComm):
260
274
  async def _smile_detect_legacy(
261
275
  self, result: etree, dsmrmain: etree, model: str
262
276
  ) -> str:
263
- """Helper-function for _smile_detect()."""
277
+ """Helper-function for _smile_detect().
278
+
279
+ Detect which type of legacy Plugwise Gateway is being connected.
280
+ """
264
281
  return_model = model
265
282
  # Stretch: find the MAC of the zigbee master_controller (= Stick)
266
283
  if (network := result.find("./module/protocols/master_controller")) is not None:
@@ -278,7 +295,7 @@ class Smile(SmileComm):
278
295
  or network is not None
279
296
  ):
280
297
  system = await self._request(SYSTEM)
281
- self.smile_fw_version = parse(system.find("./gateway/firmware").text)
298
+ self.smile_version = parse(system.find("./gateway/firmware").text)
282
299
  return_model = system.find("./gateway/product").text
283
300
  self.smile_hostname = system.find("./gateway/hostname").text
284
301
  # If wlan0 contains data it's active, so eth0 should be checked last
@@ -289,7 +306,7 @@ class Smile(SmileComm):
289
306
  # P1 legacy:
290
307
  elif dsmrmain is not None:
291
308
  status = await self._request(STATUS)
292
- self.smile_fw_version = parse(status.find("./system/version").text)
309
+ self.smile_version = parse(status.find("./system/version").text)
293
310
  return_model = status.find("./system/product").text
294
311
  self.smile_hostname = status.find("./network/hostname").text
295
312
  self.smile_mac_address = status.find("./network/mac_address").text
@@ -304,20 +321,11 @@ class Smile(SmileComm):
304
321
  self.smile_legacy = True
305
322
  return return_model
306
323
 
307
- async def full_xml_update(self) -> None:
308
- """Helper-function used for testing."""
309
- await self._smile_api.full_xml_update()
310
-
311
- def get_all_gateway_entities(self) -> None:
312
- """Helper-function used for testing."""
313
- self._smile_api.get_all_gateway_entities()
314
-
315
- async def async_update(self) -> PlugwiseData:
316
- """Update the various entities and their states."""
317
- data = PlugwiseData(devices={}, gateway={})
324
+ async def async_update(self) -> dict[str, GwEntityData]:
325
+ """Update the Plughwise Gateway entities and their data and states."""
326
+ data: dict[str, GwEntityData] = {}
318
327
  try:
319
328
  data = await self._smile_api.async_update()
320
- self.gateway_id = data.gateway["gateway_id"]
321
329
  except (DataMissingError, KeyError) as err:
322
330
  raise PlugwiseError("No Plugwise data received") from err
323
331
 
plugwise/common.py CHANGED
@@ -9,19 +9,17 @@ from typing import cast
9
9
 
10
10
  from plugwise.constants import (
11
11
  ANNA,
12
+ NONE,
12
13
  SPECIAL_PLUG_TYPES,
13
14
  SWITCH_GROUP_TYPES,
14
15
  ApplianceType,
15
16
  GwEntityData,
16
17
  ModuleData,
17
- SensorType,
18
18
  )
19
19
  from plugwise.util import (
20
- check_alternative_location,
21
20
  check_heater_central,
22
21
  check_model,
23
22
  get_vendor_name,
24
- power_data_local_format,
25
23
  return_valid,
26
24
  )
27
25
 
@@ -29,19 +27,32 @@ from defusedxml import ElementTree as etree
29
27
  from munch import Munch
30
28
 
31
29
 
30
+ def get_zigbee_data(module: etree, module_data: ModuleData, legacy: bool) -> None:
31
+ """Helper-function for _get_module_data()."""
32
+ if legacy:
33
+ # Stretches
34
+ if (router := module.find("./protocols/network_router")) is not None:
35
+ module_data["zigbee_mac_address"] = router.find("mac_address").text
36
+ # Also look for the Circle+/Stealth M+
37
+ if (coord := module.find("./protocols/network_coordinator")) is not None:
38
+ module_data["zigbee_mac_address"] = coord.find("mac_address").text
39
+ # Adam
40
+ elif (zb_node := module.find("./protocols/zig_bee_node")) is not None:
41
+ module_data["zigbee_mac_address"] = zb_node.find("mac_address").text
42
+ module_data["reachable"] = zb_node.find("reachable").text == "true"
43
+
44
+
32
45
  class SmileCommon:
33
46
  """The SmileCommon class."""
34
47
 
35
48
  def __init__(self) -> None:
36
49
  """Init."""
37
- self._appliances: etree
50
+ self._cooling_present: bool
38
51
  self._count: int
39
52
  self._domain_objects: etree
40
- self._cooling_present: bool
41
- self._heater_id: str
53
+ self._heater_id: str = NONE
42
54
  self._on_off_device: bool
43
- self._opentherm_device: bool
44
- self.gw_entities: dict[str, GwEntityData]
55
+ self.gw_entities: dict[str, GwEntityData] = {}
45
56
  self.smile_name: str
46
57
  self.smile_type: str
47
58
 
@@ -111,99 +122,6 @@ class SmileCommon:
111
122
 
112
123
  return appl
113
124
 
114
- def _collect_power_values(
115
- self, data: GwEntityData, loc: Munch, tariff: str, legacy: bool = False
116
- ) -> None:
117
- """Something."""
118
- for loc.peak_select in ("nl_peak", "nl_offpeak"):
119
- loc.locator = (
120
- f'./{loc.log_type}[type="{loc.measurement}"]/period/'
121
- f'measurement[@{tariff}="{loc.peak_select}"]'
122
- )
123
- if legacy:
124
- loc.locator = (
125
- f"./{loc.meas_list[0]}_{loc.log_type}/measurement"
126
- f'[@directionality="{loc.meas_list[1]}"][@{tariff}="{loc.peak_select}"]'
127
- )
128
-
129
- loc = self._power_data_peak_value(loc, legacy)
130
- if not loc.found:
131
- continue
132
-
133
- data = self._power_data_energy_diff(
134
- loc.measurement, loc.net_string, loc.f_val, data
135
- )
136
- key = cast(SensorType, loc.key_string)
137
- data["sensors"][key] = loc.f_val
138
-
139
- def _count_data_items(self, data: GwEntityData) -> None:
140
- """When present, count the binary_sensors, sensors and switches dict-items, don't count the dicts.
141
-
142
- Also, count the remaining single data items, the amount of dicts present have already been pre-subtracted in the previous step.
143
- """
144
- if "binary_sensors" in data:
145
- self._count += len(data["binary_sensors"]) - 1
146
- if "sensors" in data:
147
- self._count += len(data["sensors"]) - 1
148
- if "switches" in data:
149
- self._count += len(data["switches"]) - 1
150
- self._count += len(data)
151
-
152
- def _power_data_peak_value(self, loc: Munch, legacy: bool) -> Munch:
153
- """Helper-function for _power_data_from_location() and _power_data_from_modules()."""
154
- loc.found = True
155
- if loc.logs.find(loc.locator) is None:
156
- loc = check_alternative_location(loc, legacy)
157
- if not loc.found:
158
- return loc
159
-
160
- if (peak := loc.peak_select.split("_")[1]) == "offpeak":
161
- peak = "off_peak"
162
- log_found = loc.log_type.split("_")[0]
163
- loc.key_string = f"{loc.measurement}_{peak}_{log_found}"
164
- if "gas" in loc.measurement or loc.log_type == "point_meter":
165
- loc.key_string = f"{loc.measurement}_{log_found}"
166
- # Only for P1 Actual -------------------#
167
- if "phase" in loc.measurement:
168
- loc.key_string = f"{loc.measurement}"
169
- # --------------------------------------#
170
- loc.net_string = f"net_electricity_{log_found}"
171
- val = loc.logs.find(loc.locator).text
172
- loc.f_val = power_data_local_format(loc.attrs, loc.key_string, val)
173
-
174
- return loc
175
-
176
- def _power_data_energy_diff(
177
- self,
178
- measurement: str,
179
- net_string: SensorType,
180
- f_val: float | int,
181
- data: GwEntityData,
182
- ) -> GwEntityData:
183
- """Calculate differential energy."""
184
- if (
185
- "electricity" in measurement
186
- and "phase" not in measurement
187
- and "interval" not in net_string
188
- ):
189
- diff = 1
190
- if "produced" in measurement:
191
- diff = -1
192
- if net_string not in data["sensors"]:
193
- tmp_val: float | int = 0
194
- else:
195
- tmp_val = data["sensors"][net_string]
196
-
197
- if isinstance(f_val, int):
198
- tmp_val += f_val * diff
199
- else:
200
- tmp_val += float(f_val * diff)
201
- tmp_val = float(f"{round(tmp_val, 3):.3f}")
202
-
203
- data["sensors"][net_string] = tmp_val
204
-
205
- return data
206
-
207
125
  def _create_gw_entities(self, appl: Munch) -> None:
208
126
  """Helper-function for creating/updating gw_entities."""
209
127
  self.gw_entities[appl.entity_id] = {"dev_class": appl.pwclass}
@@ -324,22 +242,6 @@ class SmileCommon:
324
242
  module_data["vendor_model"] = module.find("vendor_model").text
325
243
  module_data["hardware_version"] = module.find("hardware_version").text
326
244
  module_data["firmware_version"] = module.find("firmware_version").text
327
- self._get_zigbee_data(module, module_data, legacy)
245
+ get_zigbee_data(module, module_data, legacy)
328
246
 
329
247
  return module_data
330
-
331
- def _get_zigbee_data(
332
- self, module: etree, module_data: ModuleData, legacy: bool
333
- ) -> None:
334
- """Helper-function for _get_module_data()."""
335
- if legacy:
336
- # Stretches
337
- if (router := module.find("./protocols/network_router")) is not None:
338
- module_data["zigbee_mac_address"] = router.find("mac_address").text
339
- # Also look for the Circle+/Stealth M+
340
- if (coord := module.find("./protocols/network_coordinator")) is not None:
341
- module_data["zigbee_mac_address"] = coord.find("mac_address").text
342
- # Adam
343
- elif (zb_node := module.find("./protocols/zig_bee_node")) is not None:
344
- module_data["zigbee_mac_address"] = zb_node.find("mac_address").text
345
- module_data["reachable"] = zb_node.find("reachable").text == "true"
plugwise/constants.py CHANGED
@@ -3,7 +3,6 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from collections import namedtuple
6
- from dataclasses import dataclass
7
6
  import logging
8
7
  from typing import Final, Literal, TypedDict, get_args
9
8
 
@@ -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,11 +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
plugwise/data.py CHANGED
@@ -16,6 +16,7 @@ from plugwise.constants import (
16
16
  OFF,
17
17
  ActuatorData,
18
18
  GwEntityData,
19
+ SmileProps,
19
20
  )
20
21
  from plugwise.helper import SmileHelper
21
22
  from plugwise.util import remove_empty_platform_dicts
@@ -26,31 +27,27 @@ class SmileData(SmileHelper):
26
27
 
27
28
  def __init__(self) -> None:
28
29
  """Init."""
30
+ self._smile_props: SmileProps
31
+ self._zones: dict[str, GwEntityData] = {}
29
32
  SmileHelper.__init__(self)
30
33
 
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
 
@@ -123,14 +120,15 @@ class SmileData(SmileHelper):
123
120
  ) -> None:
124
121
  """Helper-function adding or updating the Plugwise notifications."""
125
122
  if (
126
- entity_id == self.gateway_id
123
+ entity_id == self._gateway_id
127
124
  and (self._is_thermostat or self.smile_type == "power")
128
125
  ) or (
129
126
  "binary_sensors" in entity
130
127
  and "plugwise_notification" in entity["binary_sensors"]
131
128
  ):
132
129
  data["binary_sensors"]["plugwise_notification"] = bool(self._notifications)
133
- self._count += 1
130
+ data["notifications"] = self._notifications
131
+ self._count += 2
134
132
 
135
133
  def _update_for_cooling(self, entity: GwEntityData) -> None:
136
134
  """Helper-function for adding/updating various cooling-related values."""
@@ -305,7 +303,7 @@ class SmileData(SmileHelper):
305
303
 
306
304
  def check_reg_mode(self, mode: str) -> bool:
307
305
  """Helper-function for device_data_climate()."""
308
- gateway = self.gw_entities[self.gateway_id]
306
+ gateway = self.gw_entities[self._gateway_id]
309
307
  return (
310
308
  "regulation_modes" in gateway and gateway["select_regulation_mode"] == mode
311
309
  )
@@ -313,6 +311,7 @@ class SmileData(SmileHelper):
313
311
  def _get_anna_control_state(self, data: GwEntityData) -> None:
314
312
  """Set the thermostat control_state based on the opentherm/onoff device state."""
315
313
  data["control_state"] = "idle"
314
+ self._count += 1
316
315
  for entity in self.gw_entities.values():
317
316
  if entity["dev_class"] != "heater_central":
318
317
  continue