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.
- aiowiserheatapi-1.7.9/PKG-INFO +73 -0
- aiowiserheatapi-1.7.9/README.md +43 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/__init__.py +1 -1
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/const.py +19 -4
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/heating_actuator.py +50 -6
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/light.py +42 -16
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/ptc.py +4 -4
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/smartplug.py +65 -1
- aiowiserheatapi-1.7.9/aioWiserHeatAPI.egg-info/PKG-INFO +73 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI.egg-info/SOURCES.txt +5 -1
- aiowiserheatapi-1.7.9/tests/test_heating_actuator.py +103 -0
- aiowiserheatapi-1.7.9/tests/test_light.py +115 -0
- aiowiserheatapi-1.7.9/tests/test_ptc.py +74 -0
- aiowiserheatapi-1.7.9/tests/test_smartplug.py +120 -0
- aiowiserheatapi-1.7.8/PKG-INFO +0 -379
- aiowiserheatapi-1.7.8/README.md +0 -349
- aiowiserheatapi-1.7.8/aioWiserHeatAPI.egg-info/PKG-INFO +0 -379
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/LICENSE +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/automation.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/binary_sensor.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/boiler_interface.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/button_panel.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/cli.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/devices.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/discovery.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/equipments.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/evse.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/exceptions.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/heating.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/__init__.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/automations.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/battery.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/capabilities.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/cloud.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/device.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/equipment.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/extra_config.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/firmware.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/gps.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/misc.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/network.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/opentherm.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/signal.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/special_times.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/status.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/temp.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/threshold.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/uiconfiguration.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/version.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/zigbee.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/hot_water.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/moments.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/pte.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/refactor.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/rest_controller.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/room.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/roomstat.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/schedule.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/shutter.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/smartvalve.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/smokealarm.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/system.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/temp_humidity.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/ufh.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/wiserhub.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI.egg-info/dependency_links.txt +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI.egg-info/entry_points.txt +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI.egg-info/requires.txt +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI.egg-info/top_level.txt +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/setup.cfg +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/setup.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/tests/test_device_notifications.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/tests/test_equipment.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/tests/test_equipments.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/tests/test_evse.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/tests/test_rest_controller.py +0 -0
- {aiowiserheatapi-1.7.8 → aiowiserheatapi-1.7.9}/tests/test_rest_payloads.py +0 -0
- {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)
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
120
|
+
Turn on the PowerTag Control using its manual state.
|
|
121
121
|
return: boolean
|
|
122
122
|
"""
|
|
123
|
-
result = await self._send_command({"
|
|
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
|
|
130
|
+
Turn off the PowerTag Control using its manual state.
|
|
131
131
|
return: boolean
|
|
132
132
|
"""
|
|
133
|
-
result = await self._send_command({"
|
|
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
|
|
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()
|