plugwise 1.5.1a3__py3-none-any.whl → 1.6.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
@@ -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"]
plugwise/common.py CHANGED
@@ -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
 
@@ -73,13 +73,12 @@ class SmileCommon:
73
73
  appl.name = "OpenTherm"
74
74
  locator_1 = "./logs/point_log[type='flame_state']/boiler_state"
75
75
  locator_2 = "./services/boiler_state"
76
- mod_type = "boiler_state"
77
76
  # xml_1: appliance
78
77
  # xml_3: self._modules for legacy, self._domain_objects for actual
79
78
  xml_3 = return_valid(xml_3, self._domain_objects)
80
- module_data = self._get_module_data(xml_1, locator_1, mod_type, xml_3)
79
+ module_data = self._get_module_data(xml_1, locator_1, xml_3)
81
80
  if not module_data["contents"]:
82
- module_data = self._get_module_data(xml_1, locator_2, mod_type, xml_3)
81
+ module_data = self._get_module_data(xml_1, locator_2, xml_3)
83
82
  appl.vendor_name = module_data["vendor_name"]
84
83
  appl.hardware = module_data["hardware_version"]
85
84
  appl.model_id = module_data["vendor_model"] if not legacy else None
@@ -94,22 +93,22 @@ class SmileCommon:
94
93
  def _appl_thermostat_info(self, appl: Munch, xml_1: etree, xml_2: etree = None) -> Munch:
95
94
  """Helper-function for _appliance_info_finder()."""
96
95
  locator = "./logs/point_log[type='thermostat']/thermostat"
97
- mod_type = "thermostat"
98
96
  xml_2 = return_valid(xml_2, self._domain_objects)
99
- module_data = self._get_module_data(xml_1, locator, mod_type, xml_2)
97
+ module_data = self._get_module_data(xml_1, locator, xml_2)
100
98
  appl.vendor_name = module_data["vendor_name"]
101
99
  appl.model = module_data["vendor_model"]
102
100
  if appl.model != "ThermoTouch": # model_id for Anna not present as stand-alone device
103
101
  appl.model_id = appl.model
104
102
  appl.model = check_model(appl.model, appl.vendor_name)
105
103
 
104
+ appl.available = module_data["reachable"]
106
105
  appl.hardware = module_data["hardware_version"]
107
106
  appl.firmware = module_data["firmware_version"]
108
107
  appl.zigbee_mac = module_data["zigbee_mac_address"]
109
108
 
110
109
  return appl
111
110
 
112
- 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:
113
112
  """Something."""
114
113
  for loc.peak_select in ("nl_peak", "nl_offpeak"):
115
114
  loc.locator = (
@@ -132,6 +131,19 @@ class SmileCommon:
132
131
  key = cast(SensorType, loc.key_string)
133
132
  data["sensors"][key] = loc.f_val
134
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
+
135
147
  def _power_data_peak_value(self, loc: Munch, legacy: bool) -> Munch:
136
148
  """Helper-function for _power_data_from_location() and _power_data_from_modules()."""
137
149
  loc.found = True
@@ -161,8 +173,8 @@ class SmileCommon:
161
173
  measurement: str,
162
174
  net_string: SensorType,
163
175
  f_val: float | int,
164
- direct_data: DeviceData,
165
- ) -> DeviceData:
176
+ data: GwEntityData,
177
+ ) -> GwEntityData:
166
178
  """Calculate differential energy."""
167
179
  if (
168
180
  "electricity" in measurement
@@ -172,10 +184,10 @@ class SmileCommon:
172
184
  diff = 1
173
185
  if "produced" in measurement:
174
186
  diff = -1
175
- if net_string not in direct_data["sensors"]:
187
+ if net_string not in data["sensors"]:
176
188
  tmp_val: float | int = 0
177
189
  else:
178
- tmp_val = direct_data["sensors"][net_string]
190
+ tmp_val = data["sensors"][net_string]
179
191
 
180
192
  if isinstance(f_val, int):
181
193
  tmp_val += f_val * diff
@@ -183,15 +195,16 @@ class SmileCommon:
183
195
  tmp_val += float(f_val * diff)
184
196
  tmp_val = float(f"{round(tmp_val, 3):.3f}")
185
197
 
186
- direct_data["sensors"][net_string] = tmp_val
198
+ data["sensors"][net_string] = tmp_val
187
199
 
188
- return direct_data
200
+ return data
189
201
 
190
- def _create_gw_devices(self, appl: Munch) -> None:
191
- """Helper-function for creating/updating gw_devices."""
192
- 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}
193
205
  self._count += 1
194
206
  for key, value in {
207
+ "available": appl.available,
195
208
  "firmware": appl.firmware,
196
209
  "hardware": appl.hardware,
197
210
  "location": appl.location,
@@ -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.
@@ -278,15 +288,14 @@ class SmileCommon:
278
288
  self,
279
289
  xml_1: etree,
280
290
  locator: str,
281
- mod_type: str,
282
291
  xml_2: etree = None,
283
292
  legacy: bool = False,
284
- ) -> ModelData:
293
+ ) -> ModuleData:
285
294
  """Helper-function for _energy_device_info_finder() and _appliance_info_finder().
286
295
 
287
296
  Collect requested info from MODULES.
288
297
  """
289
- model_data: ModelData = {
298
+ module_data: ModuleData = {
290
299
  "contents": False,
291
300
  "firmware_version": None,
292
301
  "hardware_version": None,
@@ -295,36 +304,35 @@ class SmileCommon:
295
304
  "vendor_model": None,
296
305
  "zigbee_mac_address": None,
297
306
  }
298
- # xml_1: appliance
307
+
299
308
  if (appl_search := xml_1.find(locator)) is not None:
309
+ link_tag = appl_search.tag
300
310
  link_id = appl_search.attrib["id"]
301
- loc = f".//services/{mod_type}[@id='{link_id}']...."
302
- if legacy:
303
- loc = f".//{mod_type}[@id='{link_id}']...."
311
+ loc = f".//services/{link_tag}[@id='{link_id}']...."
304
312
  # Not possible to walrus for some reason...
305
313
  # xml_2: self._modules for legacy, self._domain_objects for actual
306
314
  search = return_valid(xml_2, self._domain_objects)
307
315
  module = search.find(loc)
308
316
  if module is not None: # pylint: disable=consider-using-assignment-expr
309
- model_data["contents"] = True
310
- get_vendor_name(module, model_data)
311
- model_data["vendor_model"] = module.find("vendor_model").text
312
- model_data["hardware_version"] = module.find("hardware_version").text
313
- model_data["firmware_version"] = module.find("firmware_version").text
314
- 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)
315
323
 
316
- return model_data
324
+ return module_data
317
325
 
318
- def _get_zigbee_data(self, module: etree, model_data: ModelData, legacy: bool) -> None:
319
- """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()."""
320
328
  if legacy:
321
329
  # Stretches
322
330
  if (router := module.find("./protocols/network_router")) is not None:
323
- model_data["zigbee_mac_address"] = router.find("mac_address").text
331
+ module_data["zigbee_mac_address"] = router.find("mac_address").text
324
332
  # Also look for the Circle+/Stealth M+
325
333
  if (coord := module.find("./protocols/network_coordinator")) is not None:
326
- model_data["zigbee_mac_address"] = coord.find("mac_address").text
334
+ module_data["zigbee_mac_address"] = coord.find("mac_address").text
327
335
  # Adam
328
336
  elif (zb_node := module.find("./protocols/zig_bee_node")) is not None:
329
- model_data["zigbee_mac_address"] = zb_node.find("mac_address").text
330
- 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"
plugwise/constants.py CHANGED
@@ -81,6 +81,7 @@ HW_MODELS: Final[dict[str, str]] = {
81
81
 
82
82
  MAX_SETPOINT: Final[float] = 30.0
83
83
  MIN_SETPOINT: Final[float] = 4.0
84
+ MODULE_LOCATOR: Final = "./logs/point_log/*[@id]"
84
85
  NONE: Final = "None"
85
86
  OFF: Final = "off"
86
87
 
@@ -123,7 +124,7 @@ P1_LEGACY_MEASUREMENTS: Final[dict[str, UOM]] = {
123
124
  # radiator_valve: 'uncorrected_temperature', 'temperature_offset'
124
125
 
125
126
  DEVICE_MEASUREMENTS: Final[dict[str, DATA | UOM]] = {
126
- "humidity": UOM(PERCENTAGE), # Specific for a Jip
127
+ "humidity": UOM(NONE), # Specific for a Jip
127
128
  "illuminance": UOM(UNIT_LUMEN), # Specific for an Anna
128
129
  "temperature": UOM(TEMP_CELSIUS), # HA Core thermostat current_temperature
129
130
  "thermostat": DATA("setpoint", TEMP_CELSIUS), # HA Core thermostat setpoint
@@ -189,6 +190,14 @@ OBSOLETE_MEASUREMENTS: Final[tuple[str, ...]] = (
189
190
  "outdoor_temperature",
190
191
  )
191
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
+
192
201
  # Literals
193
202
  SMILE_P1 = "Smile P1"
194
203
  POWER = "power"
@@ -396,8 +405,8 @@ class GatewayData(TypedDict, total=False):
396
405
  smile_name: str
397
406
 
398
407
 
399
- class ModelData(TypedDict):
400
- """The ModelData class."""
408
+ class ModuleData(TypedDict):
409
+ """The Module data class."""
401
410
 
402
411
  contents: bool
403
412
  firmware_version: str | None
@@ -425,44 +434,44 @@ class SmileBinarySensors(TypedDict, total=False):
425
434
  class SmileSensors(TypedDict, total=False):
426
435
  """Smile Sensors class."""
427
436
 
428
- battery: float
437
+ battery: int
429
438
  cooling_activation_outdoor_temperature: float
430
439
  cooling_deactivation_threshold: float
431
440
  dhw_temperature: float
432
441
  domestic_hot_water_setpoint: float
433
442
  temperature: float
434
- electricity_consumed: int
435
- electricity_consumed_interval:int
443
+ electricity_consumed: float
444
+ electricity_consumed_interval: float
436
445
  electricity_consumed_off_peak_cumulative: float
437
- electricity_consumed_off_peak_interval: int
438
- electricity_consumed_off_peak_point: int
446
+ electricity_consumed_off_peak_interval: float
447
+ electricity_consumed_off_peak_point: float
439
448
  electricity_consumed_peak_cumulative: float
440
- electricity_consumed_peak_interval: int
441
- electricity_consumed_peak_point: int
442
- electricity_consumed_point: int
443
- electricity_phase_one_consumed: int
444
- electricity_phase_two_consumed: int
445
- electricity_phase_three_consumed: int
446
- electricity_phase_one_produced: int
447
- electricity_phase_two_produced: int
448
- electricity_phase_three_produced: int
449
- electricity_produced: int
450
- electricity_produced_interval: int
449
+ electricity_consumed_peak_interval: float
450
+ electricity_consumed_peak_point: float
451
+ electricity_consumed_point: float
452
+ electricity_phase_one_consumed: float
453
+ electricity_phase_two_consumed: float
454
+ electricity_phase_three_consumed: float
455
+ electricity_phase_one_produced: float
456
+ electricity_phase_two_produced: float
457
+ electricity_phase_three_produced: float
458
+ electricity_produced: float
459
+ electricity_produced_interval: float
451
460
  electricity_produced_off_peak_cumulative: float
452
- electricity_produced_off_peak_interval: int
453
- electricity_produced_off_peak_point: int
461
+ electricity_produced_off_peak_interval: float
462
+ electricity_produced_off_peak_point: float
454
463
  electricity_produced_peak_cumulative: float
455
- electricity_produced_peak_interval: int
456
- electricity_produced_peak_point: int
457
- electricity_produced_point: int
464
+ electricity_produced_peak_interval: float
465
+ electricity_produced_peak_point: float
466
+ electricity_produced_point: float
458
467
  gas_consumed_cumulative: float
459
468
  gas_consumed_interval: float
460
469
  humidity: float
461
470
  illuminance: float
462
471
  intended_boiler_temperature: float
463
- modulation_level: float
472
+ modulation_level: int
464
473
  net_electricity_cumulative: float
465
- net_electricity_point: int
474
+ net_electricity_point: float
466
475
  outdoor_air_temperature: float
467
476
  outdoor_temperature: float
468
477
  return_temperature: float
@@ -470,7 +479,7 @@ class SmileSensors(TypedDict, total=False):
470
479
  setpoint_high: float
471
480
  setpoint_low: float
472
481
  temperature_difference: float
473
- valve_position: float
482
+ valve_position: int
474
483
  voltage_phase_one: float
475
484
  voltage_phase_two: float
476
485
  voltage_phase_three: float
@@ -491,9 +500,9 @@ class ThermoLoc(TypedDict, total=False):
491
500
  """Thermo Location class."""
492
501
 
493
502
  name: str
494
- primary: str | None
503
+ primary: list[str]
495
504
  primary_prio: int
496
- secondary: set[str]
505
+ secondary: list[str]
497
506
 
498
507
 
499
508
  class ActuatorData(TypedDict, total=False):
@@ -507,8 +516,11 @@ class ActuatorData(TypedDict, total=False):
507
516
  upper_bound: float
508
517
 
509
518
 
510
- class DeviceData(TypedDict, total=False):
511
- """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
+ """
512
524
 
513
525
  # Appliance base data
514
526
  dev_class: str
@@ -543,13 +555,13 @@ class DeviceData(TypedDict, total=False):
543
555
  select_gateway_mode: str
544
556
  select_regulation_mode: str
545
557
 
546
- # Master Thermostats
558
+ # Thermostat-related
559
+ thermostats: dict[str, list[str]]
547
560
  # Presets:
548
561
  active_preset: str | None
549
562
  preset_modes: list[str] | None
550
563
  # Schedules:
551
564
  available_schedules: list[str]
552
- last_used: str | None
553
565
  select_schedule: str
554
566
 
555
567
  climate_mode: str
@@ -570,5 +582,6 @@ class DeviceData(TypedDict, total=False):
570
582
  class PlugwiseData:
571
583
  """Plugwise data provided as output."""
572
584
 
585
+ devices: dict[str, GwEntityData]
573
586
  gateway: GatewayData
574
- devices: dict[str, DeviceData]
587
+