plugwise 1.5.2__tar.gz → 1.6.1__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-1.5.2 → plugwise-1.6.1}/PKG-INFO +3 -2
  2. {plugwise-1.5.2 → plugwise-1.6.1}/plugwise/__init__.py +10 -10
  3. {plugwise-1.5.2 → plugwise-1.6.1}/plugwise/common.py +60 -50
  4. {plugwise-1.5.2 → plugwise-1.6.1}/plugwise/constants.py +21 -9
  5. {plugwise-1.5.2 → plugwise-1.6.1}/plugwise/data.py +95 -67
  6. {plugwise-1.5.2 → plugwise-1.6.1}/plugwise/helper.py +193 -137
  7. {plugwise-1.5.2 → plugwise-1.6.1}/plugwise/legacy/data.py +23 -23
  8. {plugwise-1.5.2 → plugwise-1.6.1}/plugwise/legacy/helper.py +54 -55
  9. {plugwise-1.5.2 → plugwise-1.6.1}/plugwise/legacy/smile.py +18 -16
  10. {plugwise-1.5.2 → plugwise-1.6.1}/plugwise/smile.py +24 -21
  11. {plugwise-1.5.2 → plugwise-1.6.1}/plugwise/util.py +5 -5
  12. {plugwise-1.5.2 → plugwise-1.6.1}/plugwise.egg-info/PKG-INFO +3 -2
  13. {plugwise-1.5.2 → plugwise-1.6.1}/pyproject.toml +8 -6
  14. {plugwise-1.5.2 → plugwise-1.6.1}/tests/test_adam.py +10 -8
  15. {plugwise-1.5.2 → plugwise-1.6.1}/tests/test_anna.py +17 -21
  16. {plugwise-1.5.2 → plugwise-1.6.1}/tests/test_init.py +70 -61
  17. {plugwise-1.5.2 → plugwise-1.6.1}/tests/test_legacy_anna.py +2 -2
  18. {plugwise-1.5.2 → plugwise-1.6.1}/tests/test_legacy_p1.py +2 -3
  19. {plugwise-1.5.2 → plugwise-1.6.1}/tests/test_legacy_stretch.py +3 -3
  20. {plugwise-1.5.2 → plugwise-1.6.1}/tests/test_p1.py +2 -26
  21. {plugwise-1.5.2 → plugwise-1.6.1}/LICENSE +0 -0
  22. {plugwise-1.5.2 → plugwise-1.6.1}/README.md +0 -0
  23. {plugwise-1.5.2 → plugwise-1.6.1}/plugwise/exceptions.py +0 -0
  24. {plugwise-1.5.2 → plugwise-1.6.1}/plugwise/py.typed +0 -0
  25. {plugwise-1.5.2 → plugwise-1.6.1}/plugwise.egg-info/SOURCES.txt +0 -0
  26. {plugwise-1.5.2 → plugwise-1.6.1}/plugwise.egg-info/dependency_links.txt +0 -0
  27. {plugwise-1.5.2 → plugwise-1.6.1}/plugwise.egg-info/requires.txt +0 -0
  28. {plugwise-1.5.2 → plugwise-1.6.1}/plugwise.egg-info/top_level.txt +0 -0
  29. {plugwise-1.5.2 → plugwise-1.6.1}/setup.cfg +0 -0
  30. {plugwise-1.5.2 → plugwise-1.6.1}/setup.py +0 -0
  31. {plugwise-1.5.2 → plugwise-1.6.1}/tests/test_generic.py +0 -0
  32. {plugwise-1.5.2 → plugwise-1.6.1}/tests/test_legacy_generic.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: plugwise
3
- Version: 1.5.2
3
+ Version: 1.6.1
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
@@ -36,8 +36,9 @@ Classifier: Intended Audience :: Developers
36
36
  Classifier: License :: OSI Approved :: MIT License
37
37
  Classifier: Operating System :: OS Independent
38
38
  Classifier: Programming Language :: Python :: 3.12
39
+ Classifier: Programming Language :: Python :: 3.13
39
40
  Classifier: Topic :: Home Automation
40
- Requires-Python: >=3.11.0
41
+ Requires-Python: >=3.12.0
41
42
  Description-Content-Type: text/markdown
42
43
  License-File: LICENSE
43
44
  Requires-Dist: aiohttp
@@ -69,6 +69,7 @@ class Smile(SmileComm):
69
69
  self._elga = False
70
70
  self._is_thermostat = False
71
71
  self._last_active: dict[str, str | None] = {}
72
+ self._loc_data: dict[str, ThermoLoc] = {}
72
73
  self._on_off_device = False
73
74
  self._opentherm_device = False
74
75
  self._schedule_old_states: dict[str, dict[str, str]] = {}
@@ -76,7 +77,6 @@ class Smile(SmileComm):
76
77
  self._stretch_v2 = False
77
78
  self._target_smile: str = NONE
78
79
  self.gateway_id: str = NONE
79
- self.loc_data: dict[str, ThermoLoc] = {}
80
80
  self.smile_fw_version: Version | None = None
81
81
  self.smile_hostname: str = NONE
82
82
  self.smile_hw_version: str | None = None
@@ -134,11 +134,11 @@ class Smile(SmileComm):
134
134
  self._elga,
135
135
  self._is_thermostat,
136
136
  self._last_active,
137
+ self._loc_data,
137
138
  self._on_off_device,
138
139
  self._opentherm_device,
139
140
  self._schedule_old_states,
140
141
  self.gateway_id,
141
- self.loc_data,
142
142
  self.smile_fw_version,
143
143
  self.smile_hostname,
144
144
  self.smile_hw_version,
@@ -155,11 +155,11 @@ class Smile(SmileComm):
155
155
  self._request,
156
156
  self._websession,
157
157
  self._is_thermostat,
158
+ self._loc_data,
158
159
  self._on_off_device,
159
160
  self._opentherm_device,
160
161
  self._stretch_v2,
161
162
  self._target_smile,
162
- self.loc_data,
163
163
  self.smile_fw_version,
164
164
  self.smile_hostname,
165
165
  self.smile_hw_version,
@@ -173,7 +173,7 @@ class Smile(SmileComm):
173
173
  )
174
174
 
175
175
  # Update all endpoints on first connect
176
- await self._smile_api.full_update_device()
176
+ await self._smile_api.full_xml_update()
177
177
 
178
178
  return self.smile_version
179
179
 
@@ -298,17 +298,17 @@ class Smile(SmileComm):
298
298
  self.smile_legacy = True
299
299
  return return_model
300
300
 
301
- async def full_update_device(self) -> None:
301
+ async def full_xml_update(self) -> None:
302
302
  """Helper-function used for testing."""
303
- await self._smile_api.full_update_device()
303
+ await self._smile_api.full_xml_update()
304
304
 
305
- def get_all_devices(self) -> None:
305
+ def get_all_gateway_entities(self) -> None:
306
306
  """Helper-function used for testing."""
307
- self._smile_api.get_all_devices()
307
+ self._smile_api.get_all_gateway_entities()
308
308
 
309
309
  async def async_update(self) -> PlugwiseData:
310
- """Perform an incremental update for updating the various device states."""
311
- data = PlugwiseData({}, {})
310
+ """Update the various entities and their states."""
311
+ data = PlugwiseData(devices={}, gateway={})
312
312
  try:
313
313
  data = await self._smile_api.async_update()
314
314
  self.gateway_id = data.gateway["gateway_id"]
@@ -11,8 +11,8 @@ from plugwise.constants import (
11
11
  SPECIAL_PLUG_TYPES,
12
12
  SWITCH_GROUP_TYPES,
13
13
  ApplianceType,
14
- DeviceData,
15
- ModelData,
14
+ GwEntityData,
15
+ ModuleData,
16
16
  SensorType,
17
17
  )
18
18
  from plugwise.util import (
@@ -40,7 +40,7 @@ class SmileCommon:
40
40
  self._heater_id: str
41
41
  self._on_off_device: bool
42
42
  self._opentherm_device: bool
43
- self.gw_devices: dict[str, DeviceData]
43
+ self.gw_entities: dict[str, GwEntityData]
44
44
  self.smile_name: str
45
45
  self.smile_type: str
46
46
 
@@ -108,7 +108,7 @@ class SmileCommon:
108
108
 
109
109
  return appl
110
110
 
111
- def _collect_power_values(self, data: DeviceData, loc: Munch, tariff: str, legacy: bool = False) -> None:
111
+ def _collect_power_values(self, data: GwEntityData, loc: Munch, tariff: str, legacy: bool = False) -> None:
112
112
  """Something."""
113
113
  for loc.peak_select in ("nl_peak", "nl_offpeak"):
114
114
  loc.locator = (
@@ -131,6 +131,19 @@ class SmileCommon:
131
131
  key = cast(SensorType, loc.key_string)
132
132
  data["sensors"][key] = loc.f_val
133
133
 
134
+ def _count_data_items(self, data: GwEntityData) -> None:
135
+ """When present, count the binary_sensors, sensors and switches dict-items, don't count the dicts.
136
+
137
+ Also, count the remaining single data items, the amount of dicts present have already been pre-subtracted in the previous step.
138
+ """
139
+ if "binary_sensors" in data:
140
+ self._count += len(data["binary_sensors"]) - 1
141
+ if "sensors" in data:
142
+ self._count += len(data["sensors"]) - 1
143
+ if "switches" in data:
144
+ self._count += len(data["switches"]) - 1
145
+ self._count += len(data)
146
+
134
147
  def _power_data_peak_value(self, loc: Munch, legacy: bool) -> Munch:
135
148
  """Helper-function for _power_data_from_location() and _power_data_from_modules()."""
136
149
  loc.found = True
@@ -160,8 +173,8 @@ class SmileCommon:
160
173
  measurement: str,
161
174
  net_string: SensorType,
162
175
  f_val: float | int,
163
- direct_data: DeviceData,
164
- ) -> DeviceData:
176
+ data: GwEntityData,
177
+ ) -> GwEntityData:
165
178
  """Calculate differential energy."""
166
179
  if (
167
180
  "electricity" in measurement
@@ -171,10 +184,10 @@ class SmileCommon:
171
184
  diff = 1
172
185
  if "produced" in measurement:
173
186
  diff = -1
174
- if net_string not in direct_data["sensors"]:
187
+ if net_string not in data["sensors"]:
175
188
  tmp_val: float | int = 0
176
189
  else:
177
- tmp_val = direct_data["sensors"][net_string]
190
+ tmp_val = data["sensors"][net_string]
178
191
 
179
192
  if isinstance(f_val, int):
180
193
  tmp_val += f_val * diff
@@ -182,13 +195,13 @@ class SmileCommon:
182
195
  tmp_val += float(f_val * diff)
183
196
  tmp_val = float(f"{round(tmp_val, 3):.3f}")
184
197
 
185
- direct_data["sensors"][net_string] = tmp_val
198
+ data["sensors"][net_string] = tmp_val
186
199
 
187
- return direct_data
200
+ return data
188
201
 
189
- def _create_gw_devices(self, appl: Munch) -> None:
190
- """Helper-function for creating/updating gw_devices."""
191
- self.gw_devices[appl.dev_id] = {"dev_class": appl.pwclass}
202
+ def _create_gw_entities(self, appl: Munch) -> None:
203
+ """Helper-function for creating/updating gw_entities."""
204
+ self.gw_entities[appl.entity_id] = {"dev_class": appl.pwclass}
192
205
  self._count += 1
193
206
  for key, value in {
194
207
  "available": appl.available,
@@ -204,30 +217,30 @@ class SmileCommon:
204
217
  }.items():
205
218
  if value is not None or key == "location":
206
219
  appl_key = cast(ApplianceType, key)
207
- self.gw_devices[appl.dev_id][appl_key] = value
220
+ self.gw_entities[appl.entity_id][appl_key] = value
208
221
  self._count += 1
209
222
 
210
- def _device_data_switching_group(
211
- self, device: DeviceData, data: DeviceData
223
+ def _entity_switching_group(
224
+ self, entity: GwEntityData, data: GwEntityData
212
225
  ) -> None:
213
- """Helper-function for _get_device_data().
226
+ """Helper-function for _get_device_zone_data().
214
227
 
215
228
  Determine switching group device data.
216
229
  """
217
- if device["dev_class"] in SWITCH_GROUP_TYPES:
230
+ if entity["dev_class"] in SWITCH_GROUP_TYPES:
218
231
  counter = 0
219
- for member in device["members"]:
220
- if self.gw_devices[member]["switches"].get("relay"):
232
+ for member in entity["members"]:
233
+ if self.gw_entities[member]["switches"].get("relay"):
221
234
  counter += 1
222
235
  data["switches"]["relay"] = counter != 0
223
236
  self._count += 1
224
237
 
225
- def _get_group_switches(self) -> dict[str, DeviceData]:
226
- """Helper-function for smile.py: get_all_devices().
238
+ def _get_group_switches(self) -> dict[str, GwEntityData]:
239
+ """Helper-function for smile.py: get_all_gateway_entities().
227
240
 
228
241
  Collect switching- or pump-group info.
229
242
  """
230
- switch_groups: dict[str, DeviceData] = {}
243
+ switch_groups: dict[str, GwEntityData] = {}
231
244
  # P1 and Anna don't have switchgroups
232
245
  if self.smile_type == "power" or self.smile(ANNA):
233
246
  return switch_groups
@@ -240,25 +253,22 @@ class SmileCommon:
240
253
  group_appliances = group.findall("appliances/appliance")
241
254
  for item in group_appliances:
242
255
  # Check if members are not orphaned - stretch
243
- if item.attrib["id"] in self.gw_devices:
256
+ if item.attrib["id"] in self.gw_entities:
244
257
  members.append(item.attrib["id"])
245
258
 
246
259
  if group_type in SWITCH_GROUP_TYPES and members:
247
- switch_groups.update(
248
- {
249
- group_id: {
250
- "dev_class": group_type,
251
- "model": "Switchgroup",
252
- "name": group_name,
253
- "members": members,
254
- },
255
- },
256
- )
260
+ switch_groups[group_id] = {
261
+ "dev_class": group_type,
262
+ "model": "Switchgroup",
263
+ "name": group_name,
264
+ "members": members,
265
+ "vendor": "Plugwise",
266
+ }
257
267
  self._count += 4
258
268
 
259
269
  return switch_groups
260
270
 
261
- def _get_lock_state(self, xml: etree, data: DeviceData, stretch_v2: bool = False) -> None:
271
+ def _get_lock_state(self, xml: etree, data: GwEntityData, stretch_v2: bool = False) -> None:
262
272
  """Helper-function for _get_measurement_data().
263
273
 
264
274
  Adam & Stretches: obtain the relay-switch lock state.
@@ -280,12 +290,12 @@ class SmileCommon:
280
290
  locator: str,
281
291
  xml_2: etree = None,
282
292
  legacy: bool = False,
283
- ) -> ModelData:
293
+ ) -> ModuleData:
284
294
  """Helper-function for _energy_device_info_finder() and _appliance_info_finder().
285
295
 
286
296
  Collect requested info from MODULES.
287
297
  """
288
- model_data: ModelData = {
298
+ module_data: ModuleData = {
289
299
  "contents": False,
290
300
  "firmware_version": None,
291
301
  "hardware_version": None,
@@ -304,25 +314,25 @@ class SmileCommon:
304
314
  search = return_valid(xml_2, self._domain_objects)
305
315
  module = search.find(loc)
306
316
  if module is not None: # pylint: disable=consider-using-assignment-expr
307
- model_data["contents"] = True
308
- get_vendor_name(module, model_data)
309
- model_data["vendor_model"] = module.find("vendor_model").text
310
- model_data["hardware_version"] = module.find("hardware_version").text
311
- model_data["firmware_version"] = module.find("firmware_version").text
312
- self._get_zigbee_data(module, model_data, legacy)
317
+ module_data["contents"] = True
318
+ get_vendor_name(module, module_data)
319
+ module_data["vendor_model"] = module.find("vendor_model").text
320
+ module_data["hardware_version"] = module.find("hardware_version").text
321
+ module_data["firmware_version"] = module.find("firmware_version").text
322
+ self._get_zigbee_data(module, module_data, legacy)
313
323
 
314
- return model_data
324
+ return module_data
315
325
 
316
- def _get_zigbee_data(self, module: etree, model_data: ModelData, legacy: bool) -> None:
317
- """Helper-function for _get_model_data()."""
326
+ def _get_zigbee_data(self, module: etree, module_data: ModuleData, legacy: bool) -> None:
327
+ """Helper-function for _get_module_data()."""
318
328
  if legacy:
319
329
  # Stretches
320
330
  if (router := module.find("./protocols/network_router")) is not None:
321
- model_data["zigbee_mac_address"] = router.find("mac_address").text
331
+ module_data["zigbee_mac_address"] = router.find("mac_address").text
322
332
  # Also look for the Circle+/Stealth M+
323
333
  if (coord := module.find("./protocols/network_coordinator")) is not None:
324
- model_data["zigbee_mac_address"] = coord.find("mac_address").text
334
+ module_data["zigbee_mac_address"] = coord.find("mac_address").text
325
335
  # Adam
326
336
  elif (zb_node := module.find("./protocols/zig_bee_node")) is not None:
327
- model_data["zigbee_mac_address"] = zb_node.find("mac_address").text
328
- model_data["reachable"] = zb_node.find("reachable").text == "true"
337
+ module_data["zigbee_mac_address"] = zb_node.find("mac_address").text
338
+ module_data["reachable"] = zb_node.find("reachable").text == "true"
@@ -190,6 +190,14 @@ OBSOLETE_MEASUREMENTS: Final[tuple[str, ...]] = (
190
190
  "outdoor_temperature",
191
191
  )
192
192
 
193
+ # Zone/climate related measurements
194
+ ZONE_MEASUREMENTS: Final[dict[str, DATA | UOM]] = {
195
+ "electricity_consumed": UOM(POWER_WATT),
196
+ "electricity_produced": UOM(POWER_WATT),
197
+ "relay": UOM(NONE),
198
+ "temperature": UOM(TEMP_CELSIUS), # HA Core thermostat current_temperature
199
+ }
200
+
193
201
  # Literals
194
202
  SMILE_P1 = "Smile P1"
195
203
  POWER = "power"
@@ -397,8 +405,8 @@ class GatewayData(TypedDict, total=False):
397
405
  smile_name: str
398
406
 
399
407
 
400
- class ModelData(TypedDict):
401
- """The ModelData class."""
408
+ class ModuleData(TypedDict):
409
+ """The Module data class."""
402
410
 
403
411
  contents: bool
404
412
  firmware_version: str | None
@@ -492,9 +500,9 @@ class ThermoLoc(TypedDict, total=False):
492
500
  """Thermo Location class."""
493
501
 
494
502
  name: str
495
- primary: str | None
503
+ primary: list[str]
496
504
  primary_prio: int
497
- secondary: set[str]
505
+ secondary: list[str]
498
506
 
499
507
 
500
508
  class ActuatorData(TypedDict, total=False):
@@ -508,8 +516,11 @@ class ActuatorData(TypedDict, total=False):
508
516
  upper_bound: float
509
517
 
510
518
 
511
- class DeviceData(TypedDict, total=False):
512
- """The Device Data class, covering the collected and ordered output-data per device."""
519
+ class GwEntityData(TypedDict, total=False):
520
+ """The Gateway Entity data class.
521
+
522
+ Covering the collected output-data per device or location.
523
+ """
513
524
 
514
525
  # Appliance base data
515
526
  dev_class: str
@@ -544,13 +555,13 @@ class DeviceData(TypedDict, total=False):
544
555
  select_gateway_mode: str
545
556
  select_regulation_mode: str
546
557
 
547
- # Master Thermostats
558
+ # Thermostat-related
559
+ thermostats: dict[str, list[str]]
548
560
  # Presets:
549
561
  active_preset: str | None
550
562
  preset_modes: list[str] | None
551
563
  # Schedules:
552
564
  available_schedules: list[str]
553
- last_used: str | None
554
565
  select_schedule: str
555
566
 
556
567
  climate_mode: str
@@ -571,5 +582,6 @@ class DeviceData(TypedDict, total=False):
571
582
  class PlugwiseData:
572
583
  """Plugwise data provided as output."""
573
584
 
585
+ devices: dict[str, GwEntityData]
574
586
  gateway: GatewayData
575
- devices: dict[str, DeviceData]
587
+