aioWiserHeatAPI 1.7.2__tar.gz → 1.7.4__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 (63) hide show
  1. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/PKG-INFO +14 -6
  2. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/README.md +12 -4
  3. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI/__init__.py +1 -1
  4. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI/button_panel.py +6 -0
  5. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI/const.py +5 -1
  6. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI/devices.py +35 -0
  7. aiowiserheatapi-1.7.4/aioWiserHeatAPI/evse.py +106 -0
  8. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI/helpers/automations.py +36 -16
  9. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI/helpers/battery.py +8 -2
  10. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI/helpers/opentherm.py +6 -0
  11. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI/rest_controller.py +5 -2
  12. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI/room.py +90 -15
  13. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI/wiserhub.py +7 -0
  14. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI.egg-info/PKG-INFO +14 -6
  15. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI.egg-info/SOURCES.txt +1 -0
  16. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/setup.py +1 -1
  17. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/LICENSE +0 -0
  18. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI/automation.py +0 -0
  19. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI/binary_sensor.py +0 -0
  20. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI/boiler_interface.py +0 -0
  21. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI/cli.py +0 -0
  22. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI/discovery.py +0 -0
  23. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI/exceptions.py +0 -0
  24. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI/heating.py +0 -0
  25. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI/heating_actuator.py +0 -0
  26. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI/helpers/__init__.py +0 -0
  27. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI/helpers/capabilities.py +0 -0
  28. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI/helpers/cloud.py +0 -0
  29. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI/helpers/device.py +0 -0
  30. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI/helpers/equipment.py +0 -0
  31. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI/helpers/extra_config.py +0 -0
  32. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI/helpers/firmware.py +0 -0
  33. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI/helpers/gps.py +0 -0
  34. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI/helpers/misc.py +0 -0
  35. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI/helpers/network.py +0 -0
  36. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI/helpers/signal.py +0 -0
  37. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI/helpers/special_times.py +0 -0
  38. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI/helpers/status.py +0 -0
  39. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI/helpers/temp.py +0 -0
  40. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI/helpers/threshold.py +0 -0
  41. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI/helpers/uiconfiguration.py +0 -0
  42. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI/helpers/version.py +0 -0
  43. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI/helpers/zigbee.py +0 -0
  44. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI/hot_water.py +0 -0
  45. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI/light.py +0 -0
  46. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI/moments.py +0 -0
  47. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI/ptc.py +0 -0
  48. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI/pte.py +0 -0
  49. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI/refactor.py +0 -0
  50. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI/roomstat.py +0 -0
  51. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI/schedule.py +0 -0
  52. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI/shutter.py +0 -0
  53. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI/smartplug.py +0 -0
  54. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI/smartvalve.py +0 -0
  55. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI/smokealarm.py +0 -0
  56. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI/system.py +0 -0
  57. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI/temp_humidity.py +0 -0
  58. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI/ufh.py +0 -0
  59. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI.egg-info/dependency_links.txt +0 -0
  60. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI.egg-info/entry_points.txt +0 -0
  61. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI.egg-info/requires.txt +0 -0
  62. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/aioWiserHeatAPI.egg-info/top_level.txt +0 -0
  63. {aiowiserheatapi-1.7.2 → aiowiserheatapi-1.7.4}/setup.cfg +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aioWiserHeatAPI
3
- Version: 1.7.2
3
+ Version: 1.7.4
4
4
  Summary: An AsyncIO API for controlling the Drayton Wiser Heating system
5
5
  Home-page: https://github.com/msp1974/aioWiserHeatAPI
6
6
  Author: Mark Parker
7
7
  Author-email: msparker@sky.com
8
8
  Classifier: Development Status :: 5 - Production/Stable
9
9
  Classifier: Intended Audience :: Developers
10
- Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
11
  Classifier: License :: OSI Approved :: MIT License
12
12
  Classifier: Operating System :: OS Independent
13
13
  Requires-Python: >=3.12
@@ -28,7 +28,7 @@ Dynamic: requires-dist
28
28
  Dynamic: requires-python
29
29
  Dynamic: summary
30
30
 
31
- # Drayton Wiser Hub API Async v1.7.2
31
+ # Drayton Wiser Hub API Async v1.7.4
32
32
 
33
33
  This repository contains a simple API which queries the Drayton Wiser Heating sysystem used in the UK.
34
34
 
@@ -37,7 +37,7 @@ The API also supports Smart Plugs and initial basic functionality for Shutter an
37
37
 
38
38
  ## Requirements
39
39
 
40
- Requires a minimum of Python v3.10
40
+ Requires a minimum of Python v3.12
41
41
 
42
42
  ## Installation
43
43
 
@@ -82,9 +82,17 @@ Documentation available in [info.md](https://github.com/msp1974/wiserHeatAPIv2/b
82
82
 
83
83
  ## Changelog
84
84
 
85
- ### v1.7.2
85
+ ### v1.7.4
86
86
 
87
- - Fix: Set min f/w version of 4.42.23 for https on gen2 hubs
87
+ - Added: optional scheduled lower-temperature provider for Passive Mode
88
+ - Added: EVSE device support
89
+ - Added: Button panel battery status
90
+ - Added: Support for the `OffAfter2s` LED indicator mode
91
+ - Fix: Refine room heating state for OpenTherm systems
92
+
93
+ ### v1.7.3
94
+
95
+ - Fix: Set min f/w version of 4.42.22 for https on gen2 hubs
88
96
 
89
97
  ### v1.7.0
90
98
 
@@ -1,4 +1,4 @@
1
- # Drayton Wiser Hub API Async v1.7.2
1
+ # Drayton Wiser Hub API Async v1.7.4
2
2
 
3
3
  This repository contains a simple API which queries the Drayton Wiser Heating sysystem used in the UK.
4
4
 
@@ -7,7 +7,7 @@ The API also supports Smart Plugs and initial basic functionality for Shutter an
7
7
 
8
8
  ## Requirements
9
9
 
10
- Requires a minimum of Python v3.10
10
+ Requires a minimum of Python v3.12
11
11
 
12
12
  ## Installation
13
13
 
@@ -52,9 +52,17 @@ Documentation available in [info.md](https://github.com/msp1974/wiserHeatAPIv2/b
52
52
 
53
53
  ## Changelog
54
54
 
55
- ### v1.7.2
55
+ ### v1.7.4
56
56
 
57
- - Fix: Set min f/w version of 4.42.23 for https on gen2 hubs
57
+ - Added: optional scheduled lower-temperature provider for Passive Mode
58
+ - Added: EVSE device support
59
+ - Added: Button panel battery status
60
+ - Added: Support for the `OffAfter2s` LED indicator mode
61
+ - Fix: Refine room heating state for OpenTherm systems
62
+
63
+ ### v1.7.3
64
+
65
+ - Fix: Set min f/w version of 4.42.22 for https on gen2 hubs
58
66
 
59
67
  ### v1.7.0
60
68
 
@@ -3,6 +3,6 @@ import logging
3
3
  name = "aioWiserHeatAPI"
4
4
  __all__ = ["wiserAPI", "wiserDiscovery"]
5
5
 
6
- __VERSION__ = "1.7.2"
6
+ __VERSION__ = "1.7.4"
7
7
 
8
8
  _LOGGER = logging.getLogger(__name__)
@@ -1,11 +1,17 @@
1
1
  from typing import Any
2
2
 
3
+ from .helpers.battery import _WiserBattery
3
4
  from .helpers.device import _WiserDevice
4
5
 
5
6
 
6
7
  class _WiserButtonPanel(_WiserDevice):
7
8
  """Class representing a Wiser Button Panel"""
8
9
 
10
+ @property
11
+ def battery(self) -> _WiserBattery:
12
+ """Get the battery information for the button panel."""
13
+ return _WiserBattery(self._data)
14
+
9
15
  @property
10
16
  def number_of_gangs(self) -> list[int]:
11
17
  """Number of gangs."""
@@ -46,6 +46,7 @@ TEXT_HEATING = "Heating"
46
46
  TEXT_LEVEL = "Level"
47
47
  TEXT_LIGHTING = "Lighting"
48
48
  TEXT_MANUAL = "Manual"
49
+ TEXT_MODULATING = "Modulating"
49
50
  TEXT_NO_CHANGE = "NoChange"
50
51
  TEXT_OFF = "Off"
51
52
  TEXT_ON = "On"
@@ -68,6 +69,7 @@ TEXT_REVERSE = "ReverseWithLoad"
68
69
  TEXT_CONSISTENT = "ConsistentWithLoad"
69
70
  TEXT_ALWAYSON = "AlwaysOn"
70
71
  TEXT_ALWAYSOFF = "AlwaysOff"
72
+ TEXT_OFF_AFTER_2S = "OffAfter2s"
71
73
 
72
74
  TEXT_LASTON = "LastOnBrightness"
73
75
  TEXT_LEVELPERCENT = "LevelPercent"
@@ -81,7 +83,7 @@ SPECIAL_DAYS = [TEXT_WEEKDAYS, TEXT_WEEKENDS, TEXT_ALL]
81
83
  SPECIAL_TIMES = {"Sunrise": 3000, "Sunset": 4000}
82
84
 
83
85
  OPENTHERMV2_MIN_VERSION = Version("4.32.47")
84
- HUB_GEN2_MIN_HTTPS_VERSION = Version("4.42.23")
86
+ HUB_GEN2_MIN_HTTPS_VERSION = Version("4.42.22")
85
87
 
86
88
  # Wiser Hub Rest Api URL Constants
87
89
  WISERHUBURL = "{}:{}/data/v2/"
@@ -108,6 +110,7 @@ WISERSMOKEALARM = "SmokeAlarmDevice/{}"
108
110
  WISERBINARYSENSOR = "BinarySensor/{}"
109
111
  WISERBOILERINTERFACE = "BoilerInterface/{}"
110
112
  WISERBUTTONPANEL = "ButtonPanel/{}"
113
+ WISEREQUIPMENT = "Equipment/{}"
111
114
  WISERTHRESHOLDSENSOR = "ThresholdSensor/{}"
112
115
  WISERUICONFIGURATION = "UIConfiguration/{}"
113
116
 
@@ -144,6 +147,7 @@ class WiserLightLedIndicatorEnum(enum.Enum):
144
147
  consistent = TEXT_CONSISTENT
145
148
  allwayson = TEXT_ALWAYSON
146
149
  allwaysoff = TEXT_ALWAYSOFF
150
+ offafter2s = TEXT_OFF_AFTER_2S
147
151
 
148
152
 
149
153
  class WiserLightPowerOnBehaviourEnum(enum.Enum):
@@ -28,6 +28,7 @@ from .const import (
28
28
  WISERUFHCONTROLLER,
29
29
  WISERUICONFIGURATION,
30
30
  )
31
+ from .evse import _WiserEVSE, _WiserEVSECollection
31
32
  from .heating_actuator import _WiserHeatingActuator, _WiserHeatingActuatorCollection
32
33
  from .helpers.device import _WiserDevice
33
34
  from .helpers.threshold import (
@@ -147,6 +148,12 @@ PRODUCT_TYPE_CONFIG = {
147
148
  collection=_WiserTempHumidityCollection,
148
149
  endpoint=None,
149
150
  ),
151
+ # EVSE devices have no separate EVSE domain section. Their live data is
152
+ # published directly as an Equipment record linked to the generic Device.
153
+ "EVSE": DeviceConfig(
154
+ device_class=_WiserEVSE,
155
+ collection=_WiserEVSECollection,
156
+ ),
150
157
  }
151
158
 
152
159
  ANCILLARY_SENSOR_CONFIG = {
@@ -226,6 +233,26 @@ class _WiserDeviceCollection:
226
233
  )
227
234
  )
228
235
 
236
+ elif device_type == "EVSE":
237
+ device_config = PRODUCT_TYPE_CONFIG[device_type]
238
+ for equipment in self._equipment_data:
239
+ if equipment.get("DeviceApplicationInstanceType") != "EVSE":
240
+ continue
241
+ device_info_data = self._get_device_info_by_id(
242
+ equipment.get("DeviceApplicationInstanceId")
243
+ )
244
+ if device_info_data is None:
245
+ continue
246
+ self._device_collection[device_type]._items.append(
247
+ device_config.device_class(
248
+ self._wiser_rest_controller,
249
+ device_config.endpoint,
250
+ device_info_data,
251
+ equipment,
252
+ None,
253
+ )
254
+ )
255
+
229
256
  elif device_type_data := self._domain_data.get(device_type):
230
257
  # We have this device type in data
231
258
  device_config = PRODUCT_TYPE_CONFIG[device_type]
@@ -323,6 +350,14 @@ class _WiserDeviceCollection:
323
350
  except KeyError:
324
351
  return None
325
352
 
353
+ @property
354
+ def evse(self) -> _WiserEVSECollection:
355
+ """Return all EV charging socket devices."""
356
+ try:
357
+ return self._device_collection["EVSE"]
358
+ except KeyError:
359
+ return None
360
+
326
361
  @property
327
362
  def lights(self) -> _WiserLightCollection:
328
363
  """Return all lights"""
@@ -0,0 +1,106 @@
1
+ """Support for Wiser EV charging socket equipment."""
2
+
3
+ from .const import TEXT_UNKNOWN, WISEREQUIPMENT
4
+ from .helpers.device import _WiserDevice
5
+ from .helpers.equipment import _WiserEquipment
6
+
7
+
8
+ class _WiserEVSE(_WiserDevice):
9
+ """A Wiser EVSE represented by a Device and an Equipment record."""
10
+
11
+ @property
12
+ def equipment(self) -> _WiserEquipment:
13
+ """Return the EVSE equipment data."""
14
+ return _WiserEquipment(self._device_type_data)
15
+
16
+ @property
17
+ def equipment_id(self) -> int:
18
+ """Return the equipment record id."""
19
+ return self.equipment.id
20
+
21
+ @property
22
+ def instantaneous_power(self) -> int | None:
23
+ """Return current active power in watts."""
24
+ power = self.equipment.power
25
+ return (
26
+ power.total_active_power
27
+ if power.total_active_power is not None
28
+ else power.active_power
29
+ )
30
+
31
+ @property
32
+ def delivered_power(self) -> int | None:
33
+ """Return accumulated delivered energy in watt-hours."""
34
+ return self.equipment.power.current_summation_delivered
35
+
36
+ @property
37
+ def available_control_states(self) -> list[str]:
38
+ """Return control states accepted by the hub for this equipment."""
39
+ return self._device_type_data.get("AvailableControlState") or []
40
+
41
+ @property
42
+ def control_state(self) -> str:
43
+ """Return the current load control state."""
44
+ return self._device_type_data.get(
45
+ "ControlState",
46
+ self._device_type_data.get(
47
+ "LoadStateCommandPrio",
48
+ self._device_type_data.get("LoadStateStatus", TEXT_UNKNOWN),
49
+ ),
50
+ ) or TEXT_UNKNOWN
51
+
52
+ @property
53
+ def is_on(self) -> bool:
54
+ """Return whether charging has not been force-disabled."""
55
+ return self.control_state not in {
56
+ TEXT_UNKNOWN,
57
+ "consumptionForceOff",
58
+ "Off",
59
+ }
60
+
61
+ async def _set_control_state(self, state: str) -> bool:
62
+ """Set one of the control states advertised by the equipment."""
63
+ if state not in self.available_control_states:
64
+ raise ValueError(
65
+ f"{state} is not a valid EVSE control state. "
66
+ f"Valid states are {self.available_control_states}"
67
+ )
68
+
69
+ result = await self._wiser_rest_controller._send_command(
70
+ WISEREQUIPMENT.format(self.equipment_id), {"ControlState": state}
71
+ )
72
+ if result:
73
+ # Some hub versions return only the changed field. Preserve the
74
+ # equipment telemetry and advertised states until the next poll.
75
+ self._device_type_data = {**self._device_type_data, **result}
76
+ return True
77
+ return False
78
+
79
+ async def turn_on(self) -> bool:
80
+ """Return the EVSE to its normal self-managed state."""
81
+ return await self._set_control_state("selfManagement")
82
+
83
+ async def turn_off(self) -> bool:
84
+ """Force EVSE consumption off."""
85
+ return await self._set_control_state("consumptionForceOff")
86
+
87
+
88
+ class _WiserEVSECollection:
89
+ """Collection of Wiser EV charging socket devices."""
90
+
91
+ def __init__(self):
92
+ self._items = []
93
+
94
+ @property
95
+ def all(self) -> list[_WiserEVSE]:
96
+ """Return all EVSE devices."""
97
+ return list(self._items)
98
+
99
+ @property
100
+ def count(self) -> int:
101
+ """Return the number of EVSE devices."""
102
+ return len(self.all)
103
+
104
+ def get_by_id(self, device_id: int) -> _WiserEVSE | None:
105
+ """Return an EVSE by its physical device id."""
106
+ return next((device for device in self.all if device.id == device_id), None)
@@ -1,6 +1,4 @@
1
1
  import logging
2
-
3
- from ..const import WiserHeatingModeEnum
4
2
  from ..heating import _WiserHeatingChannelCollection
5
3
  from ..hot_water import _WiserHotwater
6
4
  from ..rest_controller import _WiserRestController
@@ -62,6 +60,9 @@ class _WiserHeatingChannelAutomations:
62
60
  passive_mode_increment = (
63
61
  self._wiser_rest_controller._api_parameters.passive_mode_increment
64
62
  )
63
+ temperature_limiter_provider = (
64
+ self._wiser_rest_controller._api_parameters.passive_mode_temperature_limiter_provider
65
+ )
65
66
  # iterate each heating channel
66
67
  for heating_channel in self._heating_channels:
67
68
  passive_rooms = [
@@ -69,7 +70,7 @@ class _WiserHeatingChannelAutomations:
69
70
  for room in self._rooms
70
71
  if room.is_passive_mode
71
72
  and room.id in heating_channel.room_ids
72
- and room.current_temperature # added to prevent error if trv offline
73
+ and room.current_temperature is not None
73
74
  ]
74
75
 
75
76
  # If no passive mode rooms
@@ -94,20 +95,39 @@ class _WiserHeatingChannelAutomations:
94
95
  for room in passive_rooms:
95
96
  # If room is boosted do not override
96
97
  if not room.is_boosted:
97
- # Set target temp to heat passive room in
98
- # increments of passive_temperature_increment (default 0.5)
99
- target_temp = min(
100
- round(
101
- (room.current_temperature + passive_mode_increment) * 2
98
+ temperature_limited = False
99
+ if temperature_limiter_provider is not None:
100
+ try:
101
+ temperature_limited = bool(
102
+ temperature_limiter_provider(room)
103
+ )
104
+ except Exception:
105
+ _LOGGER.warning(
106
+ "Ignoring invalid Passive Mode temperature "
107
+ "limiter for %s",
108
+ room.name,
109
+ exc_info=True,
110
+ )
111
+
112
+ if temperature_limited:
113
+ # External Temperature Limiter rooms keep Wiser heat
114
+ # demand at the lower threshold. Their assigned valve
115
+ # is controlled independently by the integration.
116
+ target_temp = room.passive_mode_lower_temp
117
+ else:
118
+ # Preserve the original native Passive Mode behaviour:
119
+ # modulate the room valve while another room is heating.
120
+ target_temp = min(
121
+ round(
122
+ (
123
+ room.current_temperature
124
+ + passive_mode_increment
125
+ )
126
+ * 2
127
+ )
128
+ / 2,
129
+ room.passive_mode_upper_temp,
102
130
  )
103
- / 2,
104
- (
105
- room.schedule.current_setting
106
- if room.mode == WiserHeatingModeEnum.auto.value
107
- and room.schedule
108
- else room.passive_mode_upper_temp
109
- ),
110
- )
111
131
 
112
132
  if target_temp < room.passive_mode_lower_temp:
113
133
  target_temp = room.passive_mode_lower_temp
@@ -20,14 +20,16 @@ class _WiserBattery(object):
20
20
  @property
21
21
  def level(self) -> str:
22
22
  """Get the descritpion of the battery level"""
23
- return self._data.get("BatteryLevel", TEXT_UNKNOWN)
23
+ # Some button panels omit BatteryLevel or explicitly report null.
24
+ # Normalise both cases so callers can safely treat this as text.
25
+ return self._data.get("BatteryLevel") or TEXT_UNKNOWN
24
26
 
25
27
  @property
26
28
  def percent(self) -> int:
27
29
  """Get the percent of battery remaining"""
30
+ levels = {"normal": 100, "twothirds": 66, "onethird": 33, "low": 10}
28
31
  if self.voltage is None:
29
32
  # This device does not provide battery voltage. Calc % from level text
30
- levels = {"normal": 100, "twothirds": 66, "onethird": 33, "low": 10}
31
33
  return levels.get(self.level.lower(), 0)
32
34
 
33
35
  elif self._data.get("ProductType") == "RoomStat" and self.voltage:
@@ -46,6 +48,10 @@ class _WiserBattery(object):
46
48
  if self.voltage < TRV_FULL_BATTERY_LEVEL
47
49
  else 100
48
50
  )
51
+ elif self._data.get("ProductType") == "ButtonPanel":
52
+ # The hub reports a voltage for button panels but does not expose a
53
+ # calibrated voltage curve, so use its qualitative battery level.
54
+ return levels.get(self.level.lower(), 0)
49
55
  else:
50
56
  return None
51
57
 
@@ -218,6 +218,12 @@ class _WiserOpentherm(object):
218
218
  """Get ch2FlowSetpoint"""
219
219
  return tf._from_wiser_temp(self._data.get("ch2FlowSetpoint", None), "current")
220
220
 
221
+ @property
222
+ def central_heating_active(self) -> bool:
223
+ """Return whether the boiler is operating in central-heating mode."""
224
+ slave_status = self.operational_data.slave_status
225
+ return isinstance(slave_status, int) and bool(slave_status & (1 << 1))
226
+
221
227
  @property
222
228
  def connection_status(self) -> str:
223
229
  """Get opentherm connection status"""
@@ -6,7 +6,7 @@ import json
6
6
  import re
7
7
  from dataclasses import dataclass
8
8
  from datetime import datetime
9
- from typing import Optional
9
+ from typing import Callable, Optional
10
10
 
11
11
  import aiohttp
12
12
  import aiohttp.client_exceptions
@@ -35,7 +35,10 @@ from .helpers.extra_config import _WiserExtraConfig
35
35
  class WiserAPIParams:
36
36
  """Class to hold default values"""
37
37
 
38
- passive_mode_increment: str = 0.5
38
+ passive_mode_increment: float = 0.5
39
+ passive_mode_temperature_limiter_provider: Optional[Callable] = None
40
+ passive_mode_lower_temp_provider: Optional[Callable] = None
41
+ passive_mode_upper_temp_provider: Optional[Callable] = None
39
42
  stored_manual_target_temperature_alt_source: str = "current"
40
43
  boost_temp_delta: int = DEFAULT_BOOST_DELTA
41
44
  hw_climate_mode: bool = False
@@ -35,12 +35,14 @@ class _WiserRoom(object):
35
35
  schedule: _WiserSchedule,
36
36
  devices: list,
37
37
  enable_automations: bool,
38
+ opentherm=None,
38
39
  ):
39
40
  self._wiser_rest_controller = wiser_rest_controller
40
41
  self._data = room
41
42
  self._schedule = schedule
42
43
  self._devices = devices
43
44
  self._enable_automations = enable_automations
45
+ self._opentherm = opentherm
44
46
  self._extra_config = (
45
47
  self._wiser_rest_controller._extra_config.config(
46
48
  "Rooms", str(self.id)
@@ -148,6 +150,25 @@ class _WiserRoom(object):
148
150
 
149
151
  @property
150
152
  def passive_mode_lower_temp(self) -> float:
153
+ provider = (
154
+ self._wiser_rest_controller._api_parameters.passive_mode_lower_temp_provider
155
+ )
156
+ if (
157
+ provider is not None
158
+ and self.is_passive_mode
159
+ and self.mode == WiserHeatingModeEnum.auto.value
160
+ and self.schedule
161
+ ):
162
+ try:
163
+ scheduled_lower = provider(self)
164
+ if scheduled_lower is not None:
165
+ return min(float(scheduled_lower), self.passive_mode_upper_temp)
166
+ except Exception:
167
+ _LOGGER.warning(
168
+ "Ignoring invalid scheduled Passive Mode lower temperature for %s",
169
+ self.name,
170
+ exc_info=True,
171
+ )
151
172
  if self._extra_config and "min" in self._extra_config:
152
173
  return min(self._extra_config["min"], self.passive_mode_upper_temp)
153
174
  return min(
@@ -156,23 +177,46 @@ class _WiserRoom(object):
156
177
 
157
178
  @property
158
179
  def passive_mode_upper_temp(self) -> float:
180
+ provider = (
181
+ self._wiser_rest_controller._api_parameters.passive_mode_upper_temp_provider
182
+ )
159
183
  if (
160
184
  self.is_passive_mode
161
185
  and self.mode == WiserHeatingModeEnum.auto.value
162
186
  and self.schedule
163
187
  ):
188
+ if provider is not None:
189
+ try:
190
+ override_upper = provider(self)
191
+ if override_upper is not None:
192
+ return float(override_upper)
193
+ except Exception:
194
+ _LOGGER.warning(
195
+ "Ignoring invalid Passive Mode upper temperature override for %s",
196
+ self.name,
197
+ exc_info=True,
198
+ )
164
199
  return self.schedule.current_setting
165
200
  else:
166
- if self._extra_config and "max" in self._extra_config:
167
- return self._extra_config["max"]
168
- return self._default_extra_config["max"]
201
+ return self.configured_passive_mode_upper_temp
202
+
203
+ @property
204
+ def configured_passive_mode_upper_temp(self) -> float:
205
+ """Return the static Passive upper temperature from extra config."""
206
+ if self._extra_config and "max" in self._extra_config:
207
+ return self._extra_config["max"]
208
+ return self._default_extra_config["max"]
169
209
 
170
210
  async def set_passive_mode(self, enable: bool):
171
211
  if await self._update_extra_config("passive_mode", enable):
172
212
  await self._get_extra_config()
173
213
  if not self.is_away_mode:
174
214
  # Set to min of temp range when initialised
175
- if enable and self.mode != WiserHeatingModeEnum.off.value:
215
+ if (
216
+ enable
217
+ and self.mode != WiserHeatingModeEnum.off.value
218
+ and not self._auto_uses_scheduled_passive_lower_temp
219
+ ):
176
220
  await self.set_target_temperature(
177
221
  self.passive_mode_lower_temp
178
222
  )
@@ -189,6 +233,27 @@ class _WiserRoom(object):
189
233
  "Unable to set passive mode. This maybe caused by an issue with your extra config file."
190
234
  )
191
235
 
236
+ @property
237
+ def _auto_uses_scheduled_passive_lower_temp(self) -> bool:
238
+ """Return whether an external limiter owns the Auto schedule target."""
239
+ provider = (
240
+ self._wiser_rest_controller._api_parameters.passive_mode_temperature_limiter_provider
241
+ )
242
+ if (
243
+ self.mode == WiserHeatingModeEnum.auto.value
244
+ and self.schedule is not None
245
+ and provider is not None
246
+ ):
247
+ try:
248
+ return bool(provider(self))
249
+ except Exception:
250
+ _LOGGER.warning(
251
+ "Ignoring invalid Passive Mode temperature limiter for %s",
252
+ self.name,
253
+ exc_info=True,
254
+ )
255
+ return False
256
+
192
257
  async def set_passive_mode_lower_temp(self, temp):
193
258
  if not await self._update_extra_config("min", temp):
194
259
  _LOGGER.error(
@@ -211,8 +276,9 @@ class _WiserRoom(object):
211
276
  @property
212
277
  def available_presets(self) -> list:
213
278
  """Get available preset modes"""
214
- # Remove advance schedule if no schedule exists or in passive mode
215
- if self.is_passive_mode or not self.schedule:
279
+ # Advance is available whenever the room has a schedule. Passive Mode
280
+ # advances its paired lower and upper range through the integration.
281
+ if not self.schedule:
216
282
  return [
217
283
  mode.value
218
284
  for mode in WiserPresetOptionsEnum
@@ -363,14 +429,22 @@ class _WiserRoom(object):
363
429
  else False
364
430
  )
365
431
 
432
+ @property
433
+ def is_calling_for_heat(self) -> bool | None:
434
+ """Return whether the room is requesting heat."""
435
+ if "PercentageDemand" not in self._data:
436
+ return None
437
+ return self.percentage_demand > 0
438
+
366
439
  @property
367
440
  def is_heating(self) -> bool:
368
- """Get if the room is currently heating"""
369
- return (
370
- True
371
- if self._data.get("ControlOutputState", TEXT_OFF) == TEXT_ON
372
- else False
373
- )
441
+ """Return whether central heating is currently servicing room demand."""
442
+ if self._opentherm and self._opentherm.connection_status == "Connected":
443
+ return (
444
+ self.is_calling_for_heat is True
445
+ and self._opentherm.central_heating_active
446
+ )
447
+ return self._data.get("ControlOutputState", TEXT_OFF) == TEXT_ON
374
448
 
375
449
  @property
376
450
  def manual_target_temperature(self) -> float:
@@ -404,9 +478,7 @@ class _WiserRoom(object):
404
478
  self.stored_manual_target_temperature
405
479
  )
406
480
  elif mode == WiserHeatingModeEnum.auto.value:
407
- await self._send_command(
408
- {"Mode": WiserHeatingModeEnum.auto.value}
409
- )
481
+ await self._send_command({"Mode": WiserHeatingModeEnum.auto.value})
410
482
 
411
483
  self._mode = mode
412
484
  return True
@@ -741,6 +813,7 @@ class _WiserRoomCollection:
741
813
  schedules: _WiserScheduleCollection,
742
814
  devices: _WiserDeviceCollection,
743
815
  enable_automations: bool,
816
+ opentherm=None,
744
817
  ):
745
818
  super().__init__()
746
819
  self._wiser_rest_controller = wiser_rest_controller
@@ -748,6 +821,7 @@ class _WiserRoomCollection:
748
821
  self._schedules = schedules
749
822
  self._devices = devices
750
823
  self._enable_automations = enable_automations
824
+ self._opentherm = opentherm
751
825
  self._rooms: list(_WiserRoom) = []
752
826
  self._build()
753
827
 
@@ -767,6 +841,7 @@ class _WiserRoomCollection:
767
841
  schedule[0] if len(schedule) > 0 else None,
768
842
  devices,
769
843
  self._enable_automations,
844
+ self._opentherm,
770
845
  )
771
846
  )
772
847
 
@@ -172,7 +172,13 @@ class WiserAPI:
172
172
  "ActiveSystemVersion", "1.0.0"
173
173
  )
174
174
  )
175
+ _LOGGER.debug(
176
+ "Hub Hardware Generation: %s, Firmware Version: %s",
177
+ hw_gen,
178
+ fw_version,
179
+ )
175
180
  if hw_gen == 2 and fw_version >= HUB_GEN2_MIN_HTTPS_VERSION:
181
+ _LOGGER.debug("Using HTTPS for Wiser Hub REST API calls")
176
182
  self._wiser_rest_controller.use_https = True
177
183
 
178
184
  self._network_data = await self._wiser_rest_controller.get_hub_data(
@@ -275,6 +281,7 @@ class WiserAPI:
275
281
  self._schedules.get_by_type(WiserScheduleTypeEnum.heating),
276
282
  self._devices,
277
283
  self._enable_automations,
284
+ self._system.opentherm,
278
285
  )
279
286
 
280
287
  # Hot Water
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aioWiserHeatAPI
3
- Version: 1.7.2
3
+ Version: 1.7.4
4
4
  Summary: An AsyncIO API for controlling the Drayton Wiser Heating system
5
5
  Home-page: https://github.com/msp1974/aioWiserHeatAPI
6
6
  Author: Mark Parker
7
7
  Author-email: msparker@sky.com
8
8
  Classifier: Development Status :: 5 - Production/Stable
9
9
  Classifier: Intended Audience :: Developers
10
- Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
11
  Classifier: License :: OSI Approved :: MIT License
12
12
  Classifier: Operating System :: OS Independent
13
13
  Requires-Python: >=3.12
@@ -28,7 +28,7 @@ Dynamic: requires-dist
28
28
  Dynamic: requires-python
29
29
  Dynamic: summary
30
30
 
31
- # Drayton Wiser Hub API Async v1.7.2
31
+ # Drayton Wiser Hub API Async v1.7.4
32
32
 
33
33
  This repository contains a simple API which queries the Drayton Wiser Heating sysystem used in the UK.
34
34
 
@@ -37,7 +37,7 @@ The API also supports Smart Plugs and initial basic functionality for Shutter an
37
37
 
38
38
  ## Requirements
39
39
 
40
- Requires a minimum of Python v3.10
40
+ Requires a minimum of Python v3.12
41
41
 
42
42
  ## Installation
43
43
 
@@ -82,9 +82,17 @@ Documentation available in [info.md](https://github.com/msp1974/wiserHeatAPIv2/b
82
82
 
83
83
  ## Changelog
84
84
 
85
- ### v1.7.2
85
+ ### v1.7.4
86
86
 
87
- - Fix: Set min f/w version of 4.42.23 for https on gen2 hubs
87
+ - Added: optional scheduled lower-temperature provider for Passive Mode
88
+ - Added: EVSE device support
89
+ - Added: Button panel battery status
90
+ - Added: Support for the `OffAfter2s` LED indicator mode
91
+ - Fix: Refine room heating state for OpenTherm systems
92
+
93
+ ### v1.7.3
94
+
95
+ - Fix: Set min f/w version of 4.42.22 for https on gen2 hubs
88
96
 
89
97
  ### v1.7.0
90
98
 
@@ -11,6 +11,7 @@ aioWiserHeatAPI/cli.py
11
11
  aioWiserHeatAPI/const.py
12
12
  aioWiserHeatAPI/devices.py
13
13
  aioWiserHeatAPI/discovery.py
14
+ aioWiserHeatAPI/evse.py
14
15
  aioWiserHeatAPI/exceptions.py
15
16
  aioWiserHeatAPI/heating.py
16
17
  aioWiserHeatAPI/heating_actuator.py
@@ -35,7 +35,7 @@ setuptools.setup(
35
35
  classifiers=[
36
36
  "Development Status :: 5 - Production/Stable",
37
37
  "Intended Audience :: Developers",
38
- "Programming Language :: Python :: 3.11",
38
+ "Programming Language :: Python :: 3.12",
39
39
  "License :: OSI Approved :: MIT License",
40
40
  "Operating System :: OS Independent",
41
41
  ],
File without changes