plugwise 1.6.3__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/__init__.py CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  Plugwise backend module for Home Assistant Core.
4
4
  """
5
+
5
6
  from __future__ import annotations
6
7
 
7
8
  from plugwise.constants import (
@@ -63,7 +64,7 @@ class Smile(SmileComm):
63
64
  self._timeout,
64
65
  self._username,
65
66
  self._websession,
66
- )
67
+ )
67
68
 
68
69
  self._cooling_present = False
69
70
  self._elga = False
@@ -125,52 +126,56 @@ class Smile(SmileComm):
125
126
  # Determine smile specifics
126
127
  await self._smile_detect(result, dsmrmain)
127
128
 
128
- self._smile_api = SmileAPI(
129
- self._host,
130
- self._password,
131
- self._request,
132
- self._websession,
133
- self._cooling_present,
134
- self._elga,
135
- self._is_thermostat,
136
- self._last_active,
137
- self._loc_data,
138
- self._on_off_device,
139
- self._opentherm_device,
140
- self._schedule_old_states,
141
- self.gateway_id,
142
- self.smile_fw_version,
143
- self.smile_hostname,
144
- self.smile_hw_version,
145
- self.smile_mac_address,
146
- self.smile_model,
147
- self.smile_model_id,
148
- self.smile_name,
149
- self.smile_type,
150
- self.smile_version,
151
- self._port,
152
- self._username,
153
- ) if not self.smile_legacy else SmileLegacyAPI(
154
- self._host,
155
- self._password,
156
- self._request,
157
- self._websession,
158
- self._is_thermostat,
159
- self._loc_data,
160
- self._on_off_device,
161
- self._opentherm_device,
162
- self._stretch_v2,
163
- self._target_smile,
164
- self.smile_fw_version,
165
- self.smile_hostname,
166
- self.smile_hw_version,
167
- self.smile_mac_address,
168
- self.smile_model,
169
- self.smile_name,
170
- self.smile_type,
171
- self.smile_zigbee_mac_address,
172
- self._port,
173
- self._username,
129
+ self._smile_api = (
130
+ SmileAPI(
131
+ self._host,
132
+ self._password,
133
+ self._request,
134
+ self._websession,
135
+ self._cooling_present,
136
+ self._elga,
137
+ self._is_thermostat,
138
+ self._last_active,
139
+ self._loc_data,
140
+ self._on_off_device,
141
+ self._opentherm_device,
142
+ self._schedule_old_states,
143
+ self.gateway_id,
144
+ self.smile_fw_version,
145
+ self.smile_hostname,
146
+ self.smile_hw_version,
147
+ self.smile_mac_address,
148
+ self.smile_model,
149
+ self.smile_model_id,
150
+ self.smile_name,
151
+ self.smile_type,
152
+ self.smile_version,
153
+ self._port,
154
+ self._username,
155
+ )
156
+ if not self.smile_legacy
157
+ else SmileLegacyAPI(
158
+ self._host,
159
+ self._password,
160
+ self._request,
161
+ self._websession,
162
+ self._is_thermostat,
163
+ self._loc_data,
164
+ self._on_off_device,
165
+ self._opentherm_device,
166
+ self._stretch_v2,
167
+ self._target_smile,
168
+ self.smile_fw_version,
169
+ self.smile_hostname,
170
+ self.smile_hw_version,
171
+ self.smile_mac_address,
172
+ self.smile_model,
173
+ self.smile_name,
174
+ self.smile_type,
175
+ self.smile_zigbee_mac_address,
176
+ self._port,
177
+ self._username,
178
+ )
174
179
  )
175
180
 
176
181
  # Update all endpoints on first connect
@@ -203,7 +208,7 @@ class Smile(SmileComm):
203
208
  )
204
209
  raise UnsupportedDeviceError
205
210
 
206
- version_major= str(self.smile_fw_version.major)
211
+ version_major = str(self.smile_fw_version.major)
207
212
  self._target_smile = f"{model}_v{version_major}"
208
213
  LOGGER.debug("Plugwise identified as %s", self._target_smile)
209
214
  if self._target_smile not in SMILES:
@@ -318,9 +323,9 @@ class Smile(SmileComm):
318
323
 
319
324
  return data
320
325
 
321
- ########################################################################################################
322
- ### API Set and HA Service-related Functions ###
323
- ########################################################################################################
326
+ ########################################################################################################
327
+ ### API Set and HA Service-related Functions ###
328
+ ########################################################################################################
324
329
 
325
330
  async def set_select(
326
331
  self,
@@ -333,7 +338,9 @@ class Smile(SmileComm):
333
338
  try:
334
339
  await self._smile_api.set_select(key, loc_id, option, state)
335
340
  except ConnectionFailedError as exc:
336
- raise ConnectionFailedError(f"Failed to set select option '{option}': {str(exc)}") from exc
341
+ raise ConnectionFailedError(
342
+ f"Failed to set select option '{option}': {str(exc)}"
343
+ ) from exc
337
344
 
338
345
  async def set_schedule_state(
339
346
  self,
@@ -345,8 +352,9 @@ class Smile(SmileComm):
345
352
  try:
346
353
  await self._smile_api.set_schedule_state(loc_id, state, name)
347
354
  except ConnectionFailedError as exc: # pragma no cover
348
- raise ConnectionFailedError(f"Failed to set schedule state: {str(exc)}") from exc # pragma no cover
349
-
355
+ raise ConnectionFailedError(
356
+ f"Failed to set schedule state: {str(exc)}"
357
+ ) from exc # pragma no cover
350
358
 
351
359
  async def set_preset(self, loc_id: str, preset: str) -> None:
352
360
  """Set the given Preset on the relevant Thermostat."""
@@ -360,7 +368,9 @@ class Smile(SmileComm):
360
368
  try:
361
369
  await self._smile_api.set_temperature(loc_id, items)
362
370
  except ConnectionFailedError as exc:
363
- raise ConnectionFailedError(f"Failed to set temperature: {str(exc)}") from exc
371
+ raise ConnectionFailedError(
372
+ f"Failed to set temperature: {str(exc)}"
373
+ ) from exc
364
374
 
365
375
  async def set_number(
366
376
  self,
@@ -372,14 +382,18 @@ class Smile(SmileComm):
372
382
  try:
373
383
  await self._smile_api.set_number(dev_id, key, temperature)
374
384
  except ConnectionFailedError as exc:
375
- raise ConnectionFailedError(f"Failed to set number '{key}': {str(exc)}") from exc
385
+ raise ConnectionFailedError(
386
+ f"Failed to set number '{key}': {str(exc)}"
387
+ ) from exc
376
388
 
377
389
  async def set_temperature_offset(self, dev_id: str, offset: float) -> None:
378
390
  """Set the Temperature offset for thermostats that support this feature."""
379
391
  try: # pragma no cover
380
392
  await self._smile_api.set_offset(dev_id, offset) # pragma: no cover
381
393
  except ConnectionFailedError as exc: # pragma no cover
382
- raise ConnectionFailedError(f"Failed to set temperature offset: {str(exc)}") from exc # pragma no cover
394
+ raise ConnectionFailedError(
395
+ f"Failed to set temperature offset: {str(exc)}"
396
+ ) from exc # pragma no cover
383
397
 
384
398
  async def set_switch_state(
385
399
  self, appl_id: str, members: list[str] | None, model: str, state: str
@@ -388,39 +402,51 @@ class Smile(SmileComm):
388
402
  try:
389
403
  await self._smile_api.set_switch_state(appl_id, members, model, state)
390
404
  except ConnectionFailedError as exc:
391
- raise ConnectionFailedError(f"Failed to set switch state: {str(exc)}") from exc
405
+ raise ConnectionFailedError(
406
+ f"Failed to set switch state: {str(exc)}"
407
+ ) from exc
392
408
 
393
409
  async def set_gateway_mode(self, mode: str) -> None:
394
410
  """Set the gateway mode."""
395
411
  try: # pragma no cover
396
412
  await self._smile_api.set_gateway_mode(mode) # pragma: no cover
397
413
  except ConnectionFailedError as exc: # pragma no cover
398
- raise ConnectionFailedError(f"Failed to set gateway mode: {str(exc)}") from exc # pragma no cover
414
+ raise ConnectionFailedError(
415
+ f"Failed to set gateway mode: {str(exc)}"
416
+ ) from exc # pragma no cover
399
417
 
400
418
  async def set_regulation_mode(self, mode: str) -> None:
401
419
  """Set the heating regulation mode."""
402
420
  try: # pragma no cover
403
421
  await self._smile_api.set_regulation_mode(mode) # pragma: no cover
404
422
  except ConnectionFailedError as exc: # pragma no cover
405
- raise ConnectionFailedError(f"Failed to set regulation mode: {str(exc)}") from exc # pragma no cover
423
+ raise ConnectionFailedError(
424
+ f"Failed to set regulation mode: {str(exc)}"
425
+ ) from exc # pragma no cover
406
426
 
407
427
  async def set_dhw_mode(self, mode: str) -> None:
408
428
  """Set the domestic hot water heating regulation mode."""
409
429
  try: # pragma no cover
410
430
  await self._smile_api.set_dhw_mode(mode) # pragma: no cover
411
431
  except ConnectionFailedError as exc: # pragma no cover
412
- raise ConnectionFailedError(f"Failed to set dhw mode: {str(exc)}") from exc # pragma no cover
432
+ raise ConnectionFailedError(
433
+ f"Failed to set dhw mode: {str(exc)}"
434
+ ) from exc # pragma no cover
413
435
 
414
436
  async def delete_notification(self) -> None:
415
437
  """Delete the active Plugwise Notification."""
416
438
  try:
417
439
  await self._smile_api.delete_notification()
418
440
  except ConnectionFailedError as exc:
419
- raise ConnectionFailedError(f"Failed to delete notification: {str(exc)}") from exc
441
+ raise ConnectionFailedError(
442
+ f"Failed to delete notification: {str(exc)}"
443
+ ) from exc
420
444
 
421
445
  async def reboot_gateway(self) -> None:
422
446
  """Reboot the Plugwise Gateway."""
423
447
  try:
424
448
  await self._smile_api.reboot_gateway()
425
449
  except ConnectionFailedError as exc:
426
- raise ConnectionFailedError(f"Failed to reboot gateway: {str(exc)}") from exc
450
+ raise ConnectionFailedError(
451
+ f"Failed to reboot gateway: {str(exc)}"
452
+ ) from exc
plugwise/common.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
@@ -83,21 +84,23 @@ class SmileCommon:
83
84
  appl.hardware = module_data["hardware_version"]
84
85
  appl.model_id = module_data["vendor_model"] if not legacy else None
85
86
  appl.model = (
86
- "Generic heater/cooler"
87
- if self._cooling_present
88
- else "Generic heater"
87
+ "Generic heater/cooler" if self._cooling_present else "Generic heater"
89
88
  )
90
89
 
91
90
  return appl
92
91
 
93
- def _appl_thermostat_info(self, appl: Munch, xml_1: etree, xml_2: etree = None) -> Munch:
92
+ def _appl_thermostat_info(
93
+ self, appl: Munch, xml_1: etree, xml_2: etree = None
94
+ ) -> Munch:
94
95
  """Helper-function for _appliance_info_finder()."""
95
96
  locator = "./logs/point_log[type='thermostat']/thermostat"
96
97
  xml_2 = return_valid(xml_2, self._domain_objects)
97
98
  module_data = self._get_module_data(xml_1, locator, xml_2)
98
99
  appl.vendor_name = module_data["vendor_name"]
99
100
  appl.model = module_data["vendor_model"]
100
- if appl.model != "ThermoTouch": # model_id for Anna not present as stand-alone device
101
+ if (
102
+ appl.model != "ThermoTouch"
103
+ ): # model_id for Anna not present as stand-alone device
101
104
  appl.model_id = appl.model
102
105
  appl.model = check_model(appl.model, appl.vendor_name)
103
106
 
@@ -108,7 +111,9 @@ class SmileCommon:
108
111
 
109
112
  return appl
110
113
 
111
- def _collect_power_values(self, data: GwEntityData, loc: Munch, tariff: str, legacy: bool = False) -> None:
114
+ def _collect_power_values(
115
+ self, data: GwEntityData, loc: Munch, tariff: str, legacy: bool = False
116
+ ) -> None:
112
117
  """Something."""
113
118
  for loc.peak_select in ("nl_peak", "nl_offpeak"):
114
119
  loc.locator = (
@@ -220,9 +225,7 @@ class SmileCommon:
220
225
  self.gw_entities[appl.entity_id][appl_key] = value
221
226
  self._count += 1
222
227
 
223
- def _entity_switching_group(
224
- self, entity: GwEntityData, data: GwEntityData
225
- ) -> None:
228
+ def _entity_switching_group(self, entity: GwEntityData, data: GwEntityData) -> None:
226
229
  """Helper-function for _get_device_zone_data().
227
230
 
228
231
  Determine switching group device data.
@@ -268,7 +271,9 @@ class SmileCommon:
268
271
 
269
272
  return switch_groups
270
273
 
271
- def _get_lock_state(self, xml: etree, data: GwEntityData, stretch_v2: bool = False) -> None:
274
+ def _get_lock_state(
275
+ self, xml: etree, data: GwEntityData, stretch_v2: bool = False
276
+ ) -> None:
272
277
  """Helper-function for _get_measurement_data().
273
278
 
274
279
  Adam & Stretches: obtain the relay-switch lock state.
@@ -323,7 +328,9 @@ class SmileCommon:
323
328
 
324
329
  return module_data
325
330
 
326
- def _get_zigbee_data(self, module: etree, module_data: ModuleData, legacy: bool) -> None:
331
+ def _get_zigbee_data(
332
+ self, module: etree, module_data: ModuleData, legacy: bool
333
+ ) -> None:
327
334
  """Helper-function for _get_module_data()."""
328
335
  if legacy:
329
336
  # Stretches
@@ -334,5 +341,5 @@ class SmileCommon:
334
341
  module_data["zigbee_mac_address"] = coord.find("mac_address").text
335
342
  # Adam
336
343
  elif (zb_node := module.find("./protocols/zig_bee_node")) is not None:
337
- module_data["zigbee_mac_address"] = zb_node.find("mac_address").text
338
- module_data["reachable"] = zb_node.find("reachable").text == "true"
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
@@ -1,4 +1,5 @@
1
1
  """Plugwise Smile constants."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  from collections import namedtuple
@@ -84,6 +85,7 @@ MIN_SETPOINT: Final[float] = 4.0
84
85
  MODULE_LOCATOR: Final = "./logs/point_log/*[@id]"
85
86
  NONE: Final = "None"
86
87
  OFF: Final = "off"
88
+ PRIORITY_DEVICE_CLASSES = ("heater_central", "gateway")
87
89
 
88
90
  # XML data paths
89
91
  APPLIANCES: Final = "/core/appliances"
@@ -167,9 +169,7 @@ HEATER_CENTRAL_MEASUREMENTS: Final[dict[str, DATA | UOM]] = {
167
169
  "intended_boiler_state": DATA(
168
170
  "heating_state", NONE
169
171
  ), # 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
172
+ "flame_state": UOM(NONE), # Also present when there is a single gas-heater
173
173
  "intended_boiler_temperature": UOM(
174
174
  TEMP_CELSIUS
175
175
  ), # Non-zero when heating, zero when dhw-heating
@@ -585,4 +585,3 @@ class PlugwiseData:
585
585
 
586
586
  devices: dict[str, GwEntityData]
587
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
@@ -27,7 +28,6 @@ class SmileData(SmileHelper):
27
28
  """Init."""
28
29
  SmileHelper.__init__(self)
29
30
 
30
-
31
31
  def _all_entity_data(self) -> None:
32
32
  """Helper-function for get_all_gateway_entities().
33
33
 
@@ -78,7 +78,13 @@ class SmileData(SmileHelper):
78
78
  mac_list
79
79
  and "low_battery" in entity["binary_sensors"]
80
80
  and entity["zigbee_mac_address"] in mac_list
81
- and entity["dev_class"] in ("thermo_sensor", "thermostatic_radiator_valve", "zone_thermometer", "zone_thermostat")
81
+ and entity["dev_class"]
82
+ in (
83
+ "thermo_sensor",
84
+ "thermostatic_radiator_valve",
85
+ "zone_thermometer",
86
+ "zone_thermostat",
87
+ )
82
88
  )
83
89
  if is_battery_low:
84
90
  entity["binary_sensors"]["low_battery"] = True
@@ -98,7 +104,11 @@ class SmileData(SmileHelper):
98
104
  message: str | None = notification.get("message")
99
105
  warning: str | None = notification.get("warning")
100
106
  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)):
107
+ if (
108
+ notify is not None
109
+ and all(x in notify for x in matches)
110
+ and (mac_addresses := mac_pattern.findall(notify))
111
+ ):
102
112
  mac_address = mac_addresses[0] # re.findall() outputs a list
103
113
 
104
114
  if mac_address is not None:
@@ -114,9 +124,7 @@ class SmileData(SmileHelper):
114
124
  """Helper-function adding or updating the Plugwise notifications."""
115
125
  if (
116
126
  entity_id == self.gateway_id
117
- and (
118
- self._is_thermostat or self.smile_type == "power"
119
- )
127
+ and (self._is_thermostat or self.smile_type == "power")
120
128
  ) or (
121
129
  "binary_sensors" in entity
122
130
  and "plugwise_notification" in entity["binary_sensors"]
@@ -152,7 +160,6 @@ class SmileData(SmileHelper):
152
160
  sensors["setpoint_high"] = temp_dict["setpoint_high"]
153
161
  self._count += 2 # add 4, remove 2
154
162
 
155
-
156
163
  def _get_location_data(self, loc_id: str) -> GwEntityData:
157
164
  """Helper-function for _all_entity_data() and async_update().
158
165
 
@@ -160,13 +167,14 @@ class SmileData(SmileHelper):
160
167
  """
161
168
  zone = self._zones[loc_id]
162
169
  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
170
+ data["control_state"] = "idle"
171
+ self._count += 1
172
+ if (ctrl_state := self._control_state(data, loc_id)) and str(ctrl_state) in (
173
+ "cooling",
174
+ "heating",
175
+ "preheating",
176
+ ):
177
+ data["control_state"] = str(ctrl_state)
170
178
 
171
179
  data["sensors"].pop("setpoint") # remove, only used in _control_state()
172
180
  self._count -= 1
@@ -204,6 +212,7 @@ class SmileData(SmileHelper):
204
212
  # Thermostat data for Anna (presets, temperatures etc)
205
213
  if self.smile(ANNA) and entity["dev_class"] == "thermostat":
206
214
  self._climate_data(entity_id, entity, data)
215
+ self._get_anna_control_state(data)
207
216
 
208
217
  return data
209
218
 
@@ -235,7 +244,10 @@ class SmileData(SmileHelper):
235
244
  data["binary_sensors"]["heating_state"] = self._heating_valves() != 0
236
245
  # Add cooling_enabled binary_sensor
237
246
  if "binary_sensors" in data:
238
- if "cooling_enabled" not in data["binary_sensors"] and self._cooling_present:
247
+ if (
248
+ "cooling_enabled" not in data["binary_sensors"]
249
+ and self._cooling_present
250
+ ):
239
251
  data["binary_sensors"]["cooling_enabled"] = self._cooling_enabled
240
252
 
241
253
  # Show the allowed regulation_modes and gateway_modes
@@ -248,10 +260,7 @@ class SmileData(SmileHelper):
248
260
  self._count += 1
249
261
 
250
262
  def _climate_data(
251
- self,
252
- location_id: str,
253
- entity: GwEntityData,
254
- data: GwEntityData
263
+ self, location_id: str, entity: GwEntityData, data: GwEntityData
255
264
  ) -> None:
256
265
  """Helper-function for _get_entity_data().
257
266
 
@@ -282,7 +291,9 @@ class SmileData(SmileHelper):
282
291
  if sel_schedule in (NONE, OFF):
283
292
  data["climate_mode"] = "heat"
284
293
  if self._cooling_present:
285
- data["climate_mode"] = "cool" if self.check_reg_mode("cooling") else "heat_cool"
294
+ data["climate_mode"] = (
295
+ "cool" if self.check_reg_mode("cooling") else "heat_cool"
296
+ )
286
297
 
287
298
  if self.check_reg_mode("off"):
288
299
  data["climate_mode"] = "off"
@@ -299,6 +310,19 @@ class SmileData(SmileHelper):
299
310
  "regulation_modes" in gateway and gateway["select_regulation_mode"] == mode
300
311
  )
301
312
 
313
+ def _get_anna_control_state(self, data: GwEntityData) -> None:
314
+ """Set the thermostat control_state based on the opentherm/onoff device state."""
315
+ data["control_state"] = "idle"
316
+ for entity in self.gw_entities.values():
317
+ if entity["dev_class"] != "heater_central":
318
+ continue
319
+
320
+ binary_sensors = entity["binary_sensors"]
321
+ if binary_sensors["heating_state"]:
322
+ data["control_state"] = "heating"
323
+ if binary_sensors.get("cooling_state"):
324
+ data["control_state"] = "cooling"
325
+
302
326
  def _get_schedule_states_with_off(
303
327
  self, location: str, schedules: list[str], selected: str, data: GwEntityData
304
328
  ) -> None: