aioWiserHeatAPI 1.7.7__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.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/__init__.py +1 -1
  4. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/automation.py +47 -2
  5. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/binary_sensor.py +47 -2
  6. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/const.py +30 -4
  7. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/devices.py +13 -1
  8. aiowiserheatapi-1.7.9/aioWiserHeatAPI/equipments.py +159 -0
  9. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/evse.py +10 -7
  10. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/heating_actuator.py +50 -6
  11. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/equipment.py +39 -13
  12. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/light.py +42 -16
  13. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/ptc.py +4 -4
  14. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/rest_controller.py +59 -12
  15. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/smartplug.py +65 -1
  16. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/smokealarm.py +14 -0
  17. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/wiserhub.py +13 -0
  18. aiowiserheatapi-1.7.9/aioWiserHeatAPI.egg-info/PKG-INFO +73 -0
  19. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI.egg-info/SOURCES.txt +12 -1
  20. aiowiserheatapi-1.7.9/tests/test_device_notifications.py +191 -0
  21. aiowiserheatapi-1.7.9/tests/test_equipment.py +148 -0
  22. aiowiserheatapi-1.7.9/tests/test_equipments.py +295 -0
  23. aiowiserheatapi-1.7.9/tests/test_evse.py +41 -0
  24. aiowiserheatapi-1.7.9/tests/test_heating_actuator.py +103 -0
  25. aiowiserheatapi-1.7.9/tests/test_light.py +115 -0
  26. aiowiserheatapi-1.7.9/tests/test_ptc.py +74 -0
  27. aiowiserheatapi-1.7.9/tests/test_rest_controller.py +94 -0
  28. aiowiserheatapi-1.7.9/tests/test_rest_payloads.py +73 -0
  29. aiowiserheatapi-1.7.9/tests/test_smartplug.py +120 -0
  30. aiowiserheatapi-1.7.7/PKG-INFO +0 -368
  31. aiowiserheatapi-1.7.7/README.md +0 -338
  32. aiowiserheatapi-1.7.7/aioWiserHeatAPI.egg-info/PKG-INFO +0 -368
  33. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/LICENSE +0 -0
  34. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/boiler_interface.py +0 -0
  35. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/button_panel.py +0 -0
  36. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/cli.py +0 -0
  37. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/discovery.py +0 -0
  38. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/exceptions.py +0 -0
  39. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/heating.py +0 -0
  40. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/__init__.py +0 -0
  41. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/automations.py +0 -0
  42. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/battery.py +0 -0
  43. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/capabilities.py +0 -0
  44. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/cloud.py +0 -0
  45. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/device.py +0 -0
  46. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/extra_config.py +0 -0
  47. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/firmware.py +0 -0
  48. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/gps.py +0 -0
  49. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/misc.py +0 -0
  50. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/network.py +0 -0
  51. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/opentherm.py +0 -0
  52. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/signal.py +0 -0
  53. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/special_times.py +0 -0
  54. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/status.py +0 -0
  55. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/temp.py +0 -0
  56. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/threshold.py +0 -0
  57. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/uiconfiguration.py +0 -0
  58. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/version.py +0 -0
  59. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/helpers/zigbee.py +0 -0
  60. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/hot_water.py +0 -0
  61. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/moments.py +0 -0
  62. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/pte.py +0 -0
  63. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/refactor.py +0 -0
  64. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/room.py +0 -0
  65. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/roomstat.py +0 -0
  66. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/schedule.py +0 -0
  67. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/shutter.py +0 -0
  68. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/smartvalve.py +0 -0
  69. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/system.py +0 -0
  70. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/temp_humidity.py +0 -0
  71. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI/ufh.py +0 -0
  72. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI.egg-info/dependency_links.txt +0 -0
  73. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI.egg-info/entry_points.txt +0 -0
  74. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI.egg-info/requires.txt +0 -0
  75. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/aioWiserHeatAPI.egg-info/top_level.txt +0 -0
  76. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/setup.cfg +0 -0
  77. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.9}/setup.py +0 -0
  78. {aiowiserheatapi-1.7.7 → 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.7"
6
+ __VERSION__ = "1.7.9"
7
7
 
8
8
  _LOGGER = logging.getLogger(__name__)
@@ -1,8 +1,13 @@
1
1
  import inspect
2
2
 
3
3
  from . import _LOGGER
4
- from .const import TEXT_UNKNOWN, WISERSYSTEM
5
- from .rest_controller import _WiserRestController
4
+ from .const import (
5
+ TEXT_UNKNOWN,
6
+ WISERAUTOMATION,
7
+ WISERHUBAUTOMATION,
8
+ WISERSYSTEM,
9
+ )
10
+ from .rest_controller import WiserRestActionEnum, _WiserRestController
6
11
 
7
12
 
8
13
  class _WiserAutomation:
@@ -11,6 +16,8 @@ class _WiserAutomation:
11
16
  ):
12
17
  self._wiser_rest_controller = wiser_rest_controller
13
18
  self._automation_data = automation_data
19
+ self._details = {}
20
+ self._details_loaded = False
14
21
 
15
22
  async def _send_command(self, cmd: dict) -> bool:
16
23
  """
@@ -46,10 +53,48 @@ class _WiserAutomation:
46
53
  def notification_enabled(self) -> bool:
47
54
  return self._automation_data.get("EnableNotification", False)
48
55
 
56
+ async def get_details(self, refresh: bool = False) -> dict:
57
+ """Return the automation triggers, actions and time constraints."""
58
+ if refresh or not self._details_loaded:
59
+ details = await self._wiser_rest_controller.get_hub_data(
60
+ f"{WISERHUBAUTOMATION}{self.id}",
61
+ False,
62
+ )
63
+ self._details = details if isinstance(details, dict) else {}
64
+ self._details_loaded = True
65
+ return self._details.copy()
66
+
49
67
  async def enable_notification(self):
50
68
  """Activate automation"""
51
69
  return await self._send_command({"EnableNotification": self.id})
52
70
 
71
+ async def set_notification_enabled(self, enabled: bool) -> bool:
72
+ """Enable or disable notifications for this automation."""
73
+ await self._wiser_rest_controller._send_command(
74
+ f"{WISERAUTOMATION.format(self.id)}/EnableNotification",
75
+ enabled,
76
+ )
77
+ self._automation_data["EnableNotification"] = enabled
78
+ return True
79
+
80
+ async def set_enabled(self, enabled: bool) -> bool:
81
+ """Enable or disable this automation."""
82
+ await self._wiser_rest_controller._send_command(
83
+ f"{WISERAUTOMATION.format(self.id)}/Enabled",
84
+ enabled,
85
+ )
86
+ self._automation_data["Enabled"] = enabled
87
+ return True
88
+
89
+ async def delete(self) -> bool:
90
+ """Delete this automation from the hub."""
91
+ await self._wiser_rest_controller._send_command(
92
+ WISERAUTOMATION.format(self.id),
93
+ None,
94
+ WiserRestActionEnum.DELETE,
95
+ )
96
+ return True
97
+
53
98
 
54
99
  class _WiserAutomationCollection(object):
55
100
  def __init__(
@@ -6,6 +6,7 @@ import inspect
6
6
 
7
7
  from aioWiserHeatAPI import _LOGGER
8
8
 
9
+ from .const import WiserWindowDoorEnableNotificationEnum
9
10
  from .helpers.battery import _WiserBattery
10
11
  from .helpers.device import _WiserDevice
11
12
  from .helpers.threshold import _WiserThresholdSensor
@@ -60,10 +61,54 @@ class _WiserBinarySensor(_WiserDevice):
60
61
  return self._device_type_data.get("Type")
61
62
 
62
63
  @property
63
- def enable_notification(self) -> str:
64
- """Get if notifications is enable"""
64
+ def enable_notification(self) -> str | None:
65
+ """Return the native notification mode, when supported."""
65
66
  return self._device_type_data.get("EnableNotification")
66
67
 
68
+ @property
69
+ def available_enable_notification(self) -> list[str]:
70
+ """Return the native notification modes accepted by the hub."""
71
+ return [mode.value for mode in WiserWindowDoorEnableNotificationEnum]
72
+
73
+ @property
74
+ def notification_enabled(self) -> bool | None:
75
+ """Return whether any native notification mode is enabled."""
76
+ mode = self.enable_notification
77
+ if mode is None:
78
+ return None
79
+ if isinstance(mode, bool):
80
+ return mode
81
+ return mode != WiserWindowDoorEnableNotificationEnum.none.value
82
+
83
+ async def set_enable_notification(
84
+ self,
85
+ enable_notification: WiserWindowDoorEnableNotificationEnum | str,
86
+ ) -> bool:
87
+ """Set the native notification mode for this sensor."""
88
+ if isinstance(
89
+ enable_notification, WiserWindowDoorEnableNotificationEnum
90
+ ):
91
+ enable_notification = enable_notification.value
92
+ if enable_notification not in self.available_enable_notification:
93
+ raise ValueError(
94
+ f"{enable_notification} is not a valid notification mode. "
95
+ f"Valid modes are {self.available_enable_notification}"
96
+ )
97
+ await self._wiser_rest_controller._send_command(
98
+ self._endpoint.format(self.device_type_id),
99
+ {"EnableNotification": enable_notification},
100
+ )
101
+ self._device_type_data["EnableNotification"] = enable_notification
102
+ return True
103
+
104
+ async def set_notification_enabled(self, enabled: bool) -> bool:
105
+ """Enable or disable native notifications for this sensor."""
106
+ return await self.set_enable_notification(
107
+ WiserWindowDoorEnableNotificationEnum.oneither
108
+ if enabled
109
+ else WiserWindowDoorEnableNotificationEnum.none
110
+ )
111
+
67
112
  @property
68
113
  def battery(self) -> _WiserBattery:
69
114
  """Get the battery information for the smokealarm"""
@@ -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"]
@@ -93,6 +95,7 @@ WISERHUBSCHEDULES = WISERHUBURL + "schedules/"
93
95
  WISERHUBOPENTHERM = WISERHUBURL + "opentherm/"
94
96
  WISERHUBOPENTHERMV2 = WISERHUBURL + "openTherm/"
95
97
  WISERHUBSTATUS = WISERHUBURL + "status/"
98
+ WISERHUBAUTOMATION = "{}:{}/Automation/"
96
99
  WISERSYSTEM = "System"
97
100
  WISERDEVICE = "Device/{}"
98
101
  WISERHOTWATER = "HotWater/{}"
@@ -113,6 +116,7 @@ WISERBUTTONPANEL = "ButtonPanel/{}"
113
116
  WISEREQUIPMENT = "Equipment/{}"
114
117
  WISERTHRESHOLDSENSOR = "ThresholdSensor/{}"
115
118
  WISERUICONFIGURATION = "UIConfiguration/{}"
119
+ WISERAUTOMATION = "Automation/{}"
116
120
 
117
121
 
118
122
  # Enums
@@ -131,6 +135,15 @@ class WiserTempLimitsEnum(enum.Enum):
131
135
  floorHeatingOffset = {"min": -9, "max": 9, "type": "range"}
132
136
 
133
137
 
138
+ class WiserWindowDoorEnableNotificationEnum(enum.Enum):
139
+ """Native notification modes supported by window and door sensors."""
140
+
141
+ oneither = "OnEither"
142
+ none = "None"
143
+ ontrue = "OnTrue"
144
+ onfalse = "OnFalse"
145
+
146
+
134
147
  class WiserAwayActionEnum(enum.Enum):
135
148
  off = TEXT_OFF
136
149
  nochange = TEXT_NO_CHANGE
@@ -141,7 +154,6 @@ class WiserShutterAwayActionEnum(enum.Enum):
141
154
  nochange = TEXT_NO_CHANGE
142
155
 
143
156
 
144
- # added by Lgo44
145
157
  class WiserLightLedIndicatorEnum(enum.Enum):
146
158
  reverse = TEXT_REVERSE
147
159
  consistent = TEXT_CONSISTENT
@@ -155,7 +167,21 @@ class WiserLightPowerOnBehaviourEnum(enum.Enum):
155
167
  levelpercent = TEXT_LEVELPERCENT
156
168
 
157
169
 
158
- # 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
159
185
 
160
186
 
161
187
  class WiserDeviceModeEnum(enum.Enum):
@@ -29,6 +29,7 @@ from .const import (
29
29
  WISERUICONFIGURATION,
30
30
  )
31
31
  from .evse import _WiserEVSE, _WiserEVSECollection
32
+ from .equipments import _WiserEquipmentsCollection
32
33
  from .heating_actuator import _WiserHeatingActuator, _WiserHeatingActuatorCollection
33
34
  from .helpers.device import _WiserDevice
34
35
  from .helpers.threshold import (
@@ -182,7 +183,13 @@ class _WiserDeviceCollection:
182
183
  self._wiser_rest_controller = wiser_rest_controller
183
184
  self._device_data = domain_data.get("Device", {})
184
185
  self._domain_data = domain_data
185
- self._equipment_data = domain_data.get("Equipment", {}) # Supported by v2 hub
186
+ equipment_data = domain_data.get("Equipment")
187
+ self._equipment_data = (
188
+ equipment_data if isinstance(equipment_data, list) else []
189
+ )
190
+ self._equipments = _WiserEquipmentsCollection(
191
+ wiser_rest_controller, self._equipment_data
192
+ )
186
193
  self._schedules = schedules
187
194
  self._device_collection = {}
188
195
 
@@ -358,6 +365,11 @@ class _WiserDeviceCollection:
358
365
  except KeyError:
359
366
  return None
360
367
 
368
+ @property
369
+ def equipments(self) -> _WiserEquipmentsCollection:
370
+ """Return all equipment records reported by the hub."""
371
+ return self._equipments
372
+
361
373
  @property
362
374
  def lights(self) -> _WiserLightCollection:
363
375
  """Return all lights"""
@@ -0,0 +1,159 @@
1
+ """Represent and manage equipment records returned by a Wiser hub."""
2
+
3
+ from typing import Literal
4
+
5
+ from .const import WISEREQUIPMENT
6
+ from .helpers.equipment import _WiserEquipment
7
+ from .rest_controller import WiserRestActionEnum, _WiserRestController
8
+
9
+ PowerNotificationName = Literal[
10
+ "UnderPowerNotification", "OverPowerNotification"
11
+ ]
12
+
13
+
14
+ class _WiserEquipments(_WiserEquipment):
15
+ """A single equipment record, including its writable settings."""
16
+
17
+ def __init__(
18
+ self, wiser_rest_controller: _WiserRestController, data: dict
19
+ ) -> None:
20
+ super().__init__(data)
21
+ self._wiser_rest_controller = wiser_rest_controller
22
+
23
+ async def _set_power_notification(
24
+ self,
25
+ notification_name: PowerNotificationName,
26
+ period_mins: int,
27
+ limit: int,
28
+ enabled: bool,
29
+ ) -> bool:
30
+ """Create or replace a native power notification on the hub."""
31
+ notification = {
32
+ "PeriodMins": period_mins,
33
+ "Limit": limit,
34
+ "Enabled": enabled,
35
+ }
36
+ await self._wiser_rest_controller._send_command(
37
+ WISEREQUIPMENT.format(self.id),
38
+ {notification_name: notification},
39
+ )
40
+ self._data[notification_name] = notification
41
+ return True
42
+
43
+ async def set_under_power_notification(
44
+ self, period_mins: int, limit: int, enabled: bool = True
45
+ ) -> bool:
46
+ """Create or replace the native under-power notification."""
47
+ return await self._set_power_notification(
48
+ "UnderPowerNotification", period_mins, limit, enabled
49
+ )
50
+
51
+ async def set_over_power_notification(
52
+ self, period_mins: int, limit: int, enabled: bool = True
53
+ ) -> bool:
54
+ """Create or replace the native over-power notification."""
55
+ return await self._set_power_notification(
56
+ "OverPowerNotification", period_mins, limit, enabled
57
+ )
58
+
59
+ async def _set_power_notification_enabled(
60
+ self, notification_name: PowerNotificationName, enabled: bool
61
+ ) -> bool:
62
+ """Update an existing notification without losing its settings."""
63
+ notification = self._data.get(notification_name)
64
+ if not isinstance(notification, dict):
65
+ return False
66
+
67
+ updated_notification = {
68
+ key: notification[key]
69
+ for key in ("PeriodMins", "Limit")
70
+ if key in notification
71
+ }
72
+ updated_notification["Enabled"] = enabled
73
+ await self._wiser_rest_controller._send_command(
74
+ WISEREQUIPMENT.format(self.id),
75
+ {notification_name: updated_notification},
76
+ )
77
+ notification["Enabled"] = enabled
78
+ return True
79
+
80
+ async def set_under_power_notification_enabled(self, enabled: bool) -> bool:
81
+ """Enable or disable the under-power notification."""
82
+ return await self._set_power_notification_enabled(
83
+ "UnderPowerNotification", enabled
84
+ )
85
+
86
+ async def set_over_power_notification_enabled(self, enabled: bool) -> bool:
87
+ """Enable or disable the over-power notification."""
88
+ return await self._set_power_notification_enabled(
89
+ "OverPowerNotification", enabled
90
+ )
91
+
92
+ async def _delete_power_notification(
93
+ self, notification_name: PowerNotificationName
94
+ ) -> bool:
95
+ """Delete a native power notification from the hub."""
96
+ if notification_name not in self._data:
97
+ return False
98
+
99
+ await self._wiser_rest_controller._send_command(
100
+ f"{WISEREQUIPMENT.format(self.id)}/{notification_name}/",
101
+ None,
102
+ WiserRestActionEnum.DELETE,
103
+ )
104
+ self._data.pop(notification_name, None)
105
+ return True
106
+
107
+ async def delete_under_power_notification(self) -> bool:
108
+ """Delete the native under-power notification."""
109
+ return await self._delete_power_notification("UnderPowerNotification")
110
+
111
+ async def delete_over_power_notification(self) -> bool:
112
+ """Delete the native over-power notification."""
113
+ return await self._delete_power_notification("OverPowerNotification")
114
+
115
+
116
+ class _WiserEquipmentsCollection:
117
+ """Collection of equipment records reported by the hub."""
118
+
119
+ def __init__(
120
+ self,
121
+ wiser_rest_controller: _WiserRestController,
122
+ equipments_data: list[dict] | None = None,
123
+ ) -> None:
124
+ records = equipments_data if isinstance(equipments_data, list) else []
125
+ self._items = [
126
+ _WiserEquipments(wiser_rest_controller, record)
127
+ for record in records
128
+ if isinstance(record, dict)
129
+ ]
130
+ self._items_by_id = {
131
+ item.id: item
132
+ for item in self._items
133
+ if isinstance(item._data.get("id"), int)
134
+ }
135
+
136
+ @property
137
+ def all(self) -> list[_WiserEquipments]:
138
+ """Return all equipment records."""
139
+ return list(self._items)
140
+
141
+ @property
142
+ def count(self) -> int:
143
+ """Return the number of equipment records."""
144
+ return len(self._items)
145
+
146
+ def get_by_id(self, equipment_id: int) -> _WiserEquipments | None:
147
+ """Return an equipment record by its id."""
148
+ return self._items_by_id.get(equipment_id)
149
+
150
+ def get_by_equipment_id(self, equipment_id: int) -> _WiserEquipments | None:
151
+ """Return an equipment record by its equipment id."""
152
+ return self.get_by_id(equipment_id)
153
+
154
+ def get_by_device_id(self, device_id: int) -> _WiserEquipments | None:
155
+ """Return the equipment record linked to a device instance."""
156
+ return next(
157
+ (item for item in self._items if item.device_id == device_id),
158
+ None,
159
+ )
@@ -36,7 +36,7 @@ class _WiserEVSE(_WiserDevice):
36
36
  @property
37
37
  def available_control_states(self) -> list[str]:
38
38
  """Return control states accepted by the hub for this equipment."""
39
- return self._device_type_data.get("AvailableControlState") or []
39
+ return self.equipment.available_control_states
40
40
 
41
41
  @property
42
42
  def control_state(self) -> str:
@@ -69,12 +69,15 @@ class _WiserEVSE(_WiserDevice):
69
69
  result = await self._wiser_rest_controller._send_command(
70
70
  WISEREQUIPMENT.format(self.equipment_id), {"ControlState": state}
71
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
72
+ # Successful hub commands can return an empty object or only the
73
+ # changed field. Preserve existing telemetry and reflect the requested
74
+ # state until the next poll supplies a complete equipment record.
75
+ self._device_type_data = {
76
+ **self._device_type_data,
77
+ **result,
78
+ "ControlState": result.get("ControlState", state),
79
+ }
80
+ return True
78
81
 
79
82
  async def turn_on(self) -> bool:
80
83
  """Return the EVSE to its normal self-managed state."""
@@ -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"""