aioWiserHeatAPI 1.7.8__tar.gz → 1.7.9__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 (78) hide show
  1. aiowiserheatapi-1.7.9/PKG-INFO +73 -0
  2. aiowiserheatapi-1.7.9/README.md +43 -0
  3. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/__init__.py +1 -1
  4. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/const.py +19 -4
  5. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/heating_actuator.py +50 -6
  6. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/light.py +42 -16
  7. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/ptc.py +4 -4
  8. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/smartplug.py +65 -1
  9. aiowiserheatapi-1.7.9/aioWiserHeatAPI.egg-info/PKG-INFO +73 -0
  10. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI.egg-info/SOURCES.txt +5 -1
  11. aiowiserheatapi-1.7.9/tests/test_heating_actuator.py +103 -0
  12. aiowiserheatapi-1.7.9/tests/test_light.py +115 -0
  13. aiowiserheatapi-1.7.9/tests/test_ptc.py +74 -0
  14. aiowiserheatapi-1.7.9/tests/test_smartplug.py +120 -0
  15. aiowiserheatapi-1.7.8/PKG-INFO +0 -379
  16. aiowiserheatapi-1.7.8/README.md +0 -349
  17. aiowiserheatapi-1.7.8/aioWiserHeatAPI.egg-info/PKG-INFO +0 -379
  18. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/LICENSE +0 -0
  19. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/automation.py +0 -0
  20. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/binary_sensor.py +0 -0
  21. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/boiler_interface.py +0 -0
  22. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/button_panel.py +0 -0
  23. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/cli.py +0 -0
  24. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/devices.py +0 -0
  25. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/discovery.py +0 -0
  26. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/equipments.py +0 -0
  27. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/evse.py +0 -0
  28. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/exceptions.py +0 -0
  29. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/heating.py +0 -0
  30. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/__init__.py +0 -0
  31. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/automations.py +0 -0
  32. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/battery.py +0 -0
  33. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/capabilities.py +0 -0
  34. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/cloud.py +0 -0
  35. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/device.py +0 -0
  36. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/equipment.py +0 -0
  37. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/extra_config.py +0 -0
  38. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/firmware.py +0 -0
  39. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/gps.py +0 -0
  40. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/misc.py +0 -0
  41. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/network.py +0 -0
  42. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/opentherm.py +0 -0
  43. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/signal.py +0 -0
  44. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/special_times.py +0 -0
  45. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/status.py +0 -0
  46. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/temp.py +0 -0
  47. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/threshold.py +0 -0
  48. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/uiconfiguration.py +0 -0
  49. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/version.py +0 -0
  50. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/zigbee.py +0 -0
  51. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/hot_water.py +0 -0
  52. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/moments.py +0 -0
  53. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/pte.py +0 -0
  54. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/refactor.py +0 -0
  55. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/rest_controller.py +0 -0
  56. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/room.py +0 -0
  57. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/roomstat.py +0 -0
  58. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/schedule.py +0 -0
  59. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/shutter.py +0 -0
  60. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/smartvalve.py +0 -0
  61. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/smokealarm.py +0 -0
  62. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/system.py +0 -0
  63. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/temp_humidity.py +0 -0
  64. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/ufh.py +0 -0
  65. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/wiserhub.py +0 -0
  66. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI.egg-info/dependency_links.txt +0 -0
  67. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI.egg-info/entry_points.txt +0 -0
  68. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI.egg-info/requires.txt +0 -0
  69. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI.egg-info/top_level.txt +0 -0
  70. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/setup.cfg +0 -0
  71. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/setup.py +0 -0
  72. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/tests/test_device_notifications.py +0 -0
  73. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/tests/test_equipment.py +0 -0
  74. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/tests/test_equipments.py +0 -0
  75. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/tests/test_evse.py +0 -0
  76. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/tests/test_rest_controller.py +0 -0
  77. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/tests/test_rest_payloads.py +0 -0
  78. {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/tests/test_schedule.py +0 -0
@@ -0,0 +1,73 @@
1
+ Metadata-Version: 2.4
2
+ Name: aioWiserHeatAPI
3
+ Version: 1.7.9
4
+ Summary: An AsyncIO API for controlling the Drayton Wiser Heating system
5
+ Home-page: https://github.com/msp1974/aioWiserHeatAPI
6
+ Author: Mark Parker
7
+ Author-email: msparker@sky.com
8
+ Classifier: Development Status :: 5 - Production/Stable
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Requires-Python: >=3.12
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Requires-Dist: zeroconf>=0.131.0
17
+ Requires-Dist: aiohttp>=3.9.3
18
+ Requires-Dist: aiofiles>=23.2.1
19
+ Requires-Dist: pyyaml>=6.0.1
20
+ Dynamic: author
21
+ Dynamic: author-email
22
+ Dynamic: classifier
23
+ Dynamic: description
24
+ Dynamic: description-content-type
25
+ Dynamic: home-page
26
+ Dynamic: license-file
27
+ Dynamic: requires-dist
28
+ Dynamic: requires-python
29
+ Dynamic: summary
30
+
31
+ # Drayton Wiser Hub API Async
32
+
33
+ An asynchronous Python API for the Drayton Wiser heating system used in the UK.
34
+ It supports one-, two-, and three-channel HeatHubs, Smart Plugs, shutters, lights,
35
+ and other Wiser devices.
36
+
37
+ ## Requirements
38
+
39
+ Python 3.12 or later.
40
+
41
+ ## Installation
42
+
43
+ ```sh
44
+ python -m pip install aioWiserHeatAPI
45
+ ```
46
+
47
+ ## Hub setup
48
+
49
+ ### 1. Find your HeatHub secret key
50
+
51
+ 1. Press the setup button on your HeatHub. The light will start flashing.
52
+ 2. Connect your computer or phone to the Wi-Fi network named `WiserHeatXXXXXX`,
53
+ where `XXXXXX` is the last six digits of the hub's MAC address.
54
+ 3. Open `http://192.168.8.1/secret` in a browser. Save the returned string as your
55
+ hub's secret key.
56
+ 4. Press the setup button again to return the HeatHub to normal operation.
57
+
58
+ See the [HeatHub connection guide](https://it.knightnet.org.uk/kb/nr-qa/drayton-wiser-heating-control/#controlling-the-system)
59
+ for more information.
60
+
61
+ ### 2. Find your HeatHub IP address
62
+
63
+ Use your router's connected-device list to find the HeatHub's IP address. The hub
64
+ usually identifies itself as `WiserHeatXXXXXX`.
65
+
66
+ Hub discovery is also available through the API; see
67
+ [discovery.py](aioWiserHeatAPI/discovery.py).
68
+
69
+ ## Documentation
70
+
71
+ - [API documentation](docs/info.md)
72
+ - [API implementation](aioWiserHeatAPI/wiserhub.py)
73
+ - [Release history](CHANGELOG.md)
@@ -0,0 +1,43 @@
1
+ # Drayton Wiser Hub API Async
2
+
3
+ An asynchronous Python API for the Drayton Wiser heating system used in the UK.
4
+ It supports one-, two-, and three-channel HeatHubs, Smart Plugs, shutters, lights,
5
+ and other Wiser devices.
6
+
7
+ ## Requirements
8
+
9
+ Python 3.12 or later.
10
+
11
+ ## Installation
12
+
13
+ ```sh
14
+ python -m pip install aioWiserHeatAPI
15
+ ```
16
+
17
+ ## Hub setup
18
+
19
+ ### 1. Find your HeatHub secret key
20
+
21
+ 1. Press the setup button on your HeatHub. The light will start flashing.
22
+ 2. Connect your computer or phone to the Wi-Fi network named `WiserHeatXXXXXX`,
23
+ where `XXXXXX` is the last six digits of the hub's MAC address.
24
+ 3. Open `http://192.168.8.1/secret` in a browser. Save the returned string as your
25
+ hub's secret key.
26
+ 4. Press the setup button again to return the HeatHub to normal operation.
27
+
28
+ See the [HeatHub connection guide](https://it.knightnet.org.uk/kb/nr-qa/drayton-wiser-heating-control/#controlling-the-system)
29
+ for more information.
30
+
31
+ ### 2. Find your HeatHub IP address
32
+
33
+ Use your router's connected-device list to find the HeatHub's IP address. The hub
34
+ usually identifies itself as `WiserHeatXXXXXX`.
35
+
36
+ Hub discovery is also available through the API; see
37
+ [discovery.py](aioWiserHeatAPI/discovery.py).
38
+
39
+ ## Documentation
40
+
41
+ - [API documentation](docs/info.md)
42
+ - [API implementation](aioWiserHeatAPI/wiserhub.py)
43
+ - [Release history](CHANGELOG.md)
@@ -3,6 +3,6 @@ import logging
3
3
  name = "aioWiserHeatAPI"
4
4
  __all__ = ["wiserAPI", "wiserDiscovery"]
5
5
 
6
- __VERSION__ = "1.7.8"
6
+ __VERSION__ = "1.7.9"
7
7
 
8
8
  _LOGGER = logging.getLogger(__name__)
@@ -63,7 +63,6 @@ TEXT_WEEKDAYS = "Weekdays"
63
63
  TEXT_WEEKENDS = "Weekends"
64
64
  TEXT_ALL = "All"
65
65
  TEXT_UNABLE = "Unable"
66
- # added by Lgo44
67
66
 
68
67
  TEXT_REVERSE = "ReverseWithLoad"
69
68
  TEXT_CONSISTENT = "ConsistentWithLoad"
@@ -74,7 +73,10 @@ TEXT_OFF_AFTER_2S = "OffAfter2s"
74
73
  TEXT_LASTON = "LastOnBrightness"
75
74
  TEXT_LEVELPERCENT = "LevelPercent"
76
75
 
77
- # End added by Lgo44
76
+ TEXT_RL_LED = "RL_LED"
77
+
78
+ TEXT_RELAY = "Relay"
79
+ TEXT_FIL_PILOTE = "FilPilote"
78
80
 
79
81
  # Day Value Lists
80
82
  WEEKDAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]
@@ -152,7 +154,6 @@ class WiserShutterAwayActionEnum(enum.Enum):
152
154
  nochange = TEXT_NO_CHANGE
153
155
 
154
156
 
155
- # added by Lgo44
156
157
  class WiserLightLedIndicatorEnum(enum.Enum):
157
158
  reverse = TEXT_REVERSE
158
159
  consistent = TEXT_CONSISTENT
@@ -166,7 +167,21 @@ class WiserLightPowerOnBehaviourEnum(enum.Enum):
166
167
  levelpercent = TEXT_LEVELPERCENT
167
168
 
168
169
 
169
- # End added by Lgo44
170
+ class WiserLightOutputModeEnum(enum.Enum):
171
+ auto = TEXT_AUTO
172
+ rl_led = TEXT_RL_LED
173
+
174
+
175
+ class WiserSmartPlugLedIndicatorEnum(enum.Enum):
176
+ reverse = TEXT_REVERSE
177
+ consistent = TEXT_CONSISTENT
178
+ allwayson = TEXT_ALWAYSON
179
+ allwaysoff = TEXT_ALWAYSOFF
180
+
181
+
182
+ class WiserHeatingActuatorOutputTypeEnum(enum.Enum):
183
+ relay = TEXT_RELAY
184
+ fil_pilote = TEXT_FIL_PILOTE
170
185
 
171
186
 
172
187
  class WiserDeviceModeEnum(enum.Enum):
@@ -1,4 +1,13 @@
1
- from .const import TEMP_OFF, TEXT_UNKNOWN, WISERHEATINGACTUATOR, WiserTempLimitsEnum
1
+ from typing import Union
2
+
3
+ from .const import (
4
+ TEMP_OFF,
5
+ TEXT_UNKNOWN,
6
+ WISERDEVICE,
7
+ WISERHEATINGACTUATOR,
8
+ WiserHeatingActuatorOutputTypeEnum,
9
+ WiserTempLimitsEnum,
10
+ )
2
11
  from .helpers.device import _WiserDevice
3
12
  from .helpers.equipment import _WiserEquipment
4
13
  from .helpers.temp import _WiserTemperatureFunctions as tf
@@ -115,6 +124,22 @@ class _WiserHeatingActuator(_WiserDevice):
115
124
  self._threshold_sensors: list[_WiserThresholdSensor] = []
116
125
  self._uiconfig_sensors: list[_WiserUIConfigSensor] = []
117
126
 
127
+ async def _send_command(self, cmd: dict, device_level: bool = False) -> bool:
128
+ """Send commands using the ID belonging to the requested endpoint."""
129
+ endpoint = (
130
+ WISERDEVICE.format(self.id)
131
+ if device_level
132
+ else self._endpoint.format(self.device_type_id)
133
+ )
134
+ result = await self._wiser_rest_controller._send_command(endpoint, cmd)
135
+ if not result:
136
+ return False
137
+ if device_level:
138
+ self._data = result
139
+ else:
140
+ self._device_type_data = result
141
+ return True
142
+
118
143
  @property
119
144
  def current_target_temperature(self) -> float:
120
145
  """Get the current target temperature setting"""
@@ -164,11 +189,6 @@ class _WiserHeatingActuator(_WiserDevice):
164
189
  """Get the amount of current passing through the device now"""
165
190
  return self._device_type_data.get("InstantaneousDemand", None)
166
191
 
167
- @property
168
- def output_type(self) -> str:
169
- """Get output type"""
170
- return self._device_type_data.get("OutputType", TEXT_UNKNOWN)
171
-
172
192
  @property
173
193
  def threshold_sensors(self) -> list[_WiserThresholdSensor]:
174
194
  """Get threshold sensors."""
@@ -179,6 +199,30 @@ class _WiserHeatingActuator(_WiserDevice):
179
199
  """Get ui configuration sensors."""
180
200
  return self._uiconfig_sensors
181
201
 
202
+ @property
203
+ def available_output_type(self) -> list[str]:
204
+ """Get available heating-actuator output types."""
205
+ return [output_type.value for output_type in WiserHeatingActuatorOutputTypeEnum]
206
+
207
+ @property
208
+ def output_type(self) -> str:
209
+ """Get the output type for the heating actuator."""
210
+ return self._device_type_data.get("OutputType", TEXT_UNKNOWN)
211
+
212
+ async def set_output_type(
213
+ self, output_type: Union[WiserHeatingActuatorOutputTypeEnum, str]
214
+ ) -> bool:
215
+ """Set the output type using an enum or case-insensitive string."""
216
+ if isinstance(output_type, WiserHeatingActuatorOutputTypeEnum):
217
+ output_type = output_type.value
218
+ if isinstance(output_type, str):
219
+ for mode in WiserHeatingActuatorOutputTypeEnum:
220
+ if output_type.casefold() == mode.value.casefold():
221
+ return await self._send_command({"OutputType": mode.value})
222
+ raise ValueError(
223
+ f"{output_type} is not a valid type. Valid types are {self.available_output_type}"
224
+ )
225
+
182
226
 
183
227
  class _WiserHeatingActuatorCollection(object):
184
228
  """Class holding all wiser heating actuators"""
@@ -10,6 +10,7 @@ from .const import (
10
10
  WiserDeviceModeEnum,
11
11
  WiserLightLedIndicatorEnum,
12
12
  WiserLightPowerOnBehaviourEnum,
13
+ WiserLightOutputModeEnum,
13
14
  )
14
15
  from .helpers.device import _WiserElectricalDevice
15
16
  from .helpers.misc import is_value_in_list
@@ -169,6 +170,21 @@ class _WiserLight(_WiserElectricalDevice):
169
170
 
170
171
  # end added by LGO
171
172
 
173
+ @property
174
+ def manual_level(self) -> int:
175
+ """Get manual level of light"""
176
+ return self._device_type_data.get("ManualLevel", 0)
177
+
178
+ @property
179
+ def scheduled_percentage(self) -> int:
180
+ """Get the scheduled percentage for the light"""
181
+ return self._data.get("ScheduledPercentage", 0)
182
+
183
+ @property
184
+ def target_percentage(self) -> int:
185
+ """Get target percentage brightness of light"""
186
+ return self._device_type_data.get("TargetPercentage", 0)
187
+
172
188
  @property
173
189
  def target_state(self) -> int:
174
190
  """Get target state of light"""
@@ -216,11 +232,6 @@ class _WiserDimmableLight(_WiserLight):
216
232
  else:
217
233
  raise ValueError("Brightness level percentage must be between 0 and 100")
218
234
 
219
- @property
220
- def manual_level(self) -> int:
221
- """Get manual level of light"""
222
- return self._device_type_data.get("ManualLevel", 0)
223
-
224
235
  @property
225
236
  def override_level(self) -> int:
226
237
  """Get override level of light"""
@@ -232,16 +243,6 @@ class _WiserDimmableLight(_WiserLight):
232
243
  # TODO: Add setter for min max values
233
244
  return _WiserOutputRange(self._device_type_data.get("OutputRange", None))
234
245
 
235
- @property
236
- def scheduled_percentage(self) -> int:
237
- """Get the scheduled percentage for the light"""
238
- return self._data.get("ScheduledPercentage", 0)
239
-
240
- @property
241
- def target_percentage(self) -> int:
242
- """Get target percentage brightness of light"""
243
- return self._device_type_data.get("TargetPercentage", 0)
244
-
245
246
  # added by LGO
246
247
  # Hub V2 new features
247
248
 
@@ -268,10 +269,35 @@ class _WiserDimmableLight(_WiserLight):
268
269
  )
269
270
 
270
271
  @property
271
- def power_on_level(self) -> int:
272
+ def power_on_level(self) -> int | None:
272
273
  """Get power on level for the light"""
273
274
  return self._device_type_data.get("PowerOnLevel", None)
274
275
 
276
+ async def set_power_on_level(self, level: int) -> bool:
277
+ """Set power on level to a whole percentage between 0 and 100."""
278
+ if isinstance(level, bool) or not isinstance(level, int) or not 0 <= level <= 100:
279
+ raise ValueError("Power on level must be an integer between 0 and 100")
280
+ return await self._send_command({"PowerOnLevel": level})
281
+
282
+ @property
283
+ def available_output_mode(self) -> list[str]:
284
+ """Get available output modes."""
285
+ return [mode.value for mode in WiserLightOutputModeEnum]
286
+
287
+ async def set_output_mode(
288
+ self, output_mode: Union[WiserLightOutputModeEnum, str]
289
+ ) -> bool:
290
+ """Set output mode, accepting enum values or case-insensitive strings."""
291
+ if isinstance(output_mode, WiserLightOutputModeEnum):
292
+ output_mode = output_mode.value
293
+ if isinstance(output_mode, str):
294
+ for mode in WiserLightOutputModeEnum:
295
+ if output_mode.casefold() == mode.value.casefold():
296
+ return await self._send_command({"OutputMode": mode.value})
297
+ raise ValueError(
298
+ f"{output_mode} is not a valid mode. Valid modes are {self.available_output_mode}"
299
+ )
300
+
275
301
  # end added by LGO
276
302
 
277
303
 
@@ -117,20 +117,20 @@ class _WiserPowerTagControl(_WiserElectricalDevice):
117
117
 
118
118
  async def turn_on(self) -> bool:
119
119
  """
120
- Turn on the smart plug
120
+ Turn on the PowerTag Control using its manual state.
121
121
  return: boolean
122
122
  """
123
- result = await self._send_command({"RequestOutput": TEXT_ON})
123
+ result = await self._send_command({"ManualState": TEXT_ON})
124
124
  if result:
125
125
  self._output_state = TEXT_ON
126
126
  return result
127
127
 
128
128
  async def turn_off(self) -> bool:
129
129
  """
130
- Turn off the smart plug
130
+ Turn off the PowerTag Control using its manual state.
131
131
  return: boolean
132
132
  """
133
- result = await self._send_command({"RequestOutput": TEXT_OFF})
133
+ result = await self._send_command({"ManualState": TEXT_OFF})
134
134
  if result:
135
135
  self._output_state = TEXT_OFF
136
136
  return result
@@ -1,4 +1,13 @@
1
- from .const import TEXT_OFF, TEXT_ON, TEXT_UNKNOWN, WiserDeviceModeEnum
1
+ from typing import Union
2
+
3
+ from .const import (
4
+ TEXT_OFF,
5
+ TEXT_ON,
6
+ TEXT_UNKNOWN,
7
+ WISERDEVICE,
8
+ WiserDeviceModeEnum,
9
+ WiserSmartPlugLedIndicatorEnum,
10
+ )
2
11
  from .helpers.device import _WiserElectricalDevice
3
12
  from .helpers.equipment import _WiserEquipment
4
13
 
@@ -6,6 +15,22 @@ from .helpers.equipment import _WiserEquipment
6
15
  class _WiserSmartPlug(_WiserElectricalDevice):
7
16
  """Class representing a Wiser Smart Plug device"""
8
17
 
18
+ async def _send_command(self, cmd: dict, device_level: bool = False) -> bool:
19
+ """Send commands using the ID belonging to the requested endpoint."""
20
+ endpoint = (
21
+ WISERDEVICE.format(self.id)
22
+ if device_level
23
+ else self._endpoint.format(self.device_type_id)
24
+ )
25
+ result = await self._wiser_rest_controller._send_command(endpoint, cmd)
26
+ if not result:
27
+ return False
28
+ if device_level:
29
+ self._data = result
30
+ else:
31
+ self._device_type_data = result
32
+ return True
33
+
9
34
  @property
10
35
  def control_source(self) -> str:
11
36
  """Get the current control source of the smart plug"""
@@ -55,6 +80,45 @@ class _WiserSmartPlug(_WiserElectricalDevice):
55
80
  """Get the current scheduled state of the smart plug"""
56
81
  return self._device_type_data.get("ScheduledState", TEXT_UNKNOWN)
57
82
 
83
+ @property
84
+ def is_led_indicator_supported(self) -> bool:
85
+ """Return whether the smart plug supports LED indicator configuration."""
86
+ return bool(self._device_type_data.get("IsLedIndicatorSupported", False))
87
+
88
+ @property
89
+ def available_led_indicator(self) -> list[str]:
90
+ """Get available LED indicator modes."""
91
+ return [mode.value for mode in WiserSmartPlugLedIndicatorEnum]
92
+
93
+ @property
94
+ def led_indicator(self) -> str:
95
+ """Get the LED indicator mode for the smart plug."""
96
+ return self._device_type_data.get("LedIndicator", TEXT_UNKNOWN)
97
+
98
+ async def set_led_indicator(
99
+ self, led_indicator: Union[WiserSmartPlugLedIndicatorEnum, str]
100
+ ) -> bool:
101
+ """Set the LED indicator using an enum or case-insensitive string."""
102
+ if isinstance(led_indicator, WiserSmartPlugLedIndicatorEnum):
103
+ led_indicator = led_indicator.value
104
+ if isinstance(led_indicator, str):
105
+ for mode in WiserSmartPlugLedIndicatorEnum:
106
+ if led_indicator.casefold() == mode.value.casefold():
107
+ return await self._send_command({"LedIndicator": mode.value})
108
+ raise ValueError(
109
+ f"{led_indicator} is not a valid mode. Valid modes are {self.available_led_indicator}"
110
+ )
111
+
112
+ @property
113
+ def next_scheduled_date(self) -> str:
114
+ """Get the next scheduled date for the smart plug"""
115
+ return self._device_type_data.get("NextScheduledDate", TEXT_UNKNOWN)
116
+
117
+ @property
118
+ def next_scheduled_end_date(self) -> str:
119
+ """Get the next scheduled end date for the smart plug"""
120
+ return self._device_type_data.get("NextScheduledEndDate", TEXT_UNKNOWN)
121
+
58
122
  async def turn_on(self) -> bool:
59
123
  """
60
124
  Turn on the smart plug
@@ -0,0 +1,73 @@
1
+ Metadata-Version: 2.4
2
+ Name: aioWiserHeatAPI
3
+ Version: 1.7.9
4
+ Summary: An AsyncIO API for controlling the Drayton Wiser Heating system
5
+ Home-page: https://github.com/msp1974/aioWiserHeatAPI
6
+ Author: Mark Parker
7
+ Author-email: msparker@sky.com
8
+ Classifier: Development Status :: 5 - Production/Stable
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Requires-Python: >=3.12
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Requires-Dist: zeroconf>=0.131.0
17
+ Requires-Dist: aiohttp>=3.9.3
18
+ Requires-Dist: aiofiles>=23.2.1
19
+ Requires-Dist: pyyaml>=6.0.1
20
+ Dynamic: author
21
+ Dynamic: author-email
22
+ Dynamic: classifier
23
+ Dynamic: description
24
+ Dynamic: description-content-type
25
+ Dynamic: home-page
26
+ Dynamic: license-file
27
+ Dynamic: requires-dist
28
+ Dynamic: requires-python
29
+ Dynamic: summary
30
+
31
+ # Drayton Wiser Hub API Async
32
+
33
+ An asynchronous Python API for the Drayton Wiser heating system used in the UK.
34
+ It supports one-, two-, and three-channel HeatHubs, Smart Plugs, shutters, lights,
35
+ and other Wiser devices.
36
+
37
+ ## Requirements
38
+
39
+ Python 3.12 or later.
40
+
41
+ ## Installation
42
+
43
+ ```sh
44
+ python -m pip install aioWiserHeatAPI
45
+ ```
46
+
47
+ ## Hub setup
48
+
49
+ ### 1. Find your HeatHub secret key
50
+
51
+ 1. Press the setup button on your HeatHub. The light will start flashing.
52
+ 2. Connect your computer or phone to the Wi-Fi network named `WiserHeatXXXXXX`,
53
+ where `XXXXXX` is the last six digits of the hub's MAC address.
54
+ 3. Open `http://192.168.8.1/secret` in a browser. Save the returned string as your
55
+ hub's secret key.
56
+ 4. Press the setup button again to return the HeatHub to normal operation.
57
+
58
+ See the [HeatHub connection guide](https://it.knightnet.org.uk/kb/nr-qa/drayton-wiser-heating-control/#controlling-the-system)
59
+ for more information.
60
+
61
+ ### 2. Find your HeatHub IP address
62
+
63
+ Use your router's connected-device list to find the HeatHub's IP address. The hub
64
+ usually identifies itself as `WiserHeatXXXXXX`.
65
+
66
+ Hub discovery is also available through the API; see
67
+ [discovery.py](aioWiserHeatAPI/discovery.py).
68
+
69
+ ## Documentation
70
+
71
+ - [API documentation](docs/info.md)
72
+ - [API implementation](aioWiserHeatAPI/wiserhub.py)
73
+ - [Release history](CHANGELOG.md)
@@ -65,6 +65,10 @@ tests/test_device_notifications.py
65
65
  tests/test_equipment.py
66
66
  tests/test_equipments.py
67
67
  tests/test_evse.py
68
+ tests/test_heating_actuator.py
69
+ tests/test_light.py
70
+ tests/test_ptc.py
68
71
  tests/test_rest_controller.py
69
72
  tests/test_rest_payloads.py
70
- tests/test_schedule.py
73
+ tests/test_schedule.py
74
+ tests/test_smartplug.py
@@ -0,0 +1,103 @@
1
+ """Heating-actuator output type validation and command routing tests."""
2
+
3
+ import unittest
4
+ from unittest.mock import AsyncMock
5
+
6
+ from aioWiserHeatAPI.const import TEXT_UNKNOWN, WiserHeatingActuatorOutputTypeEnum
7
+ from aioWiserHeatAPI.heating_actuator import _WiserHeatingActuator
8
+
9
+
10
+ def make_actuator(data=None, response=False):
11
+ controller = AsyncMock()
12
+ controller._send_command.return_value = response
13
+ actuator = _WiserHeatingActuator(
14
+ controller,
15
+ "HeatingActuator/{}",
16
+ {"id": 12},
17
+ {"id": 4, **(data or {})},
18
+ None,
19
+ )
20
+ return actuator, controller
21
+
22
+
23
+ class WiserHeatingActuatorPropertiesTest(unittest.TestCase):
24
+ """Preserve reported output types and missing-field defaults."""
25
+
26
+ def test_missing_output_type(self) -> None:
27
+ actuator, _ = make_actuator()
28
+ self.assertEqual(actuator.output_type, TEXT_UNKNOWN)
29
+
30
+ def test_reported_output_type(self) -> None:
31
+ for value in ("Relay", "FilPilote", "FutureType"):
32
+ with self.subTest(value=value):
33
+ actuator, _ = make_actuator({"OutputType": value})
34
+ self.assertEqual(actuator.output_type, value)
35
+
36
+ def test_available_output_types(self) -> None:
37
+ actuator, _ = make_actuator()
38
+ self.assertEqual(actuator.available_output_type, ["Relay", "FilPilote"])
39
+
40
+
41
+ class WiserHeatingActuatorCommandsTest(unittest.IsolatedAsyncioTestCase):
42
+ """Validate commands and distinguish actuator IDs from generic device IDs."""
43
+
44
+ async def test_output_types_send_canonical_values_and_update_state(self) -> None:
45
+ for mode in WiserHeatingActuatorOutputTypeEnum:
46
+ for value in (mode, mode.value, mode.value.lower(), mode.value.upper()):
47
+ with self.subTest(value=value):
48
+ actuator, controller = make_actuator(
49
+ {"OutputType": "Relay"},
50
+ {"id": 4, "OutputType": mode.value},
51
+ )
52
+ self.assertTrue(await actuator.set_output_type(value))
53
+ controller._send_command.assert_awaited_once_with(
54
+ "HeatingActuator/4", {"OutputType": mode.value}
55
+ )
56
+ self.assertEqual(actuator.output_type, mode.value)
57
+ self.assertEqual(actuator.id, 12)
58
+
59
+ async def test_invalid_output_types_never_send(self) -> None:
60
+ for value in ("", "invalid", " Relay ", None, True, 1, []):
61
+ with self.subTest(value=value):
62
+ actuator, controller = make_actuator({"OutputType": "Relay"})
63
+ with self.assertRaises(ValueError):
64
+ await actuator.set_output_type(value)
65
+ controller._send_command.assert_not_awaited()
66
+ self.assertEqual(actuator.output_type, "Relay")
67
+
68
+ async def test_failed_output_type_command_preserves_state(self) -> None:
69
+ actuator, controller = make_actuator({"OutputType": "Relay"})
70
+ self.assertFalse(await actuator.set_output_type("FilPilote"))
71
+ controller._send_command.assert_awaited_once_with(
72
+ "HeatingActuator/4", {"OutputType": "FilPilote"}
73
+ )
74
+ self.assertEqual(actuator.output_type, "Relay")
75
+ self.assertEqual(actuator.device_type_id, 4)
76
+
77
+ async def test_device_commands_use_generic_device_id(self) -> None:
78
+ actuator, controller = make_actuator(
79
+ {"OutputType": "Relay"}, {"id": 12, "DeviceLockEnabled": True}
80
+ )
81
+ await actuator.set_device_lock_enabled(True)
82
+ controller._send_command.assert_awaited_once_with(
83
+ "Device/12", {"DeviceLockEnabled": True}
84
+ )
85
+ self.assertTrue(actuator.device_lock_enabled)
86
+ self.assertEqual(actuator.output_type, "Relay")
87
+ self.assertEqual(actuator.device_type_id, 4)
88
+
89
+ async def test_failed_device_commands_preserve_state(self) -> None:
90
+ actuator, controller = make_actuator({"OutputType": "Relay"})
91
+ self.assertFalse(
92
+ await actuator._send_command({"DeviceLockEnabled": True}, True)
93
+ )
94
+ controller._send_command.assert_awaited_once_with(
95
+ "Device/12", {"DeviceLockEnabled": True}
96
+ )
97
+ self.assertFalse(actuator.device_lock_enabled)
98
+ self.assertEqual(actuator.id, 12)
99
+ self.assertEqual(actuator.output_type, "Relay")
100
+
101
+
102
+ if __name__ == "__main__":
103
+ unittest.main()