aioWiserHeatAPI 1.7.7__tar.gz → 1.7.8__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 (71) hide show
  1. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/PKG-INFO +13 -2
  2. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/README.md +12 -1
  3. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/__init__.py +1 -1
  4. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/automation.py +47 -2
  5. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/binary_sensor.py +47 -2
  6. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/const.py +11 -0
  7. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/devices.py +13 -1
  8. aiowiserheatapi-1.7.8/aioWiserHeatAPI/equipments.py +159 -0
  9. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/evse.py +10 -7
  10. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/helpers/equipment.py +39 -13
  11. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/rest_controller.py +59 -12
  12. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/smokealarm.py +14 -0
  13. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/wiserhub.py +13 -0
  14. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI.egg-info/PKG-INFO +13 -2
  15. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI.egg-info/SOURCES.txt +7 -0
  16. aiowiserheatapi-1.7.8/tests/test_device_notifications.py +191 -0
  17. aiowiserheatapi-1.7.8/tests/test_equipment.py +148 -0
  18. aiowiserheatapi-1.7.8/tests/test_equipments.py +295 -0
  19. aiowiserheatapi-1.7.8/tests/test_evse.py +41 -0
  20. aiowiserheatapi-1.7.8/tests/test_rest_controller.py +94 -0
  21. aiowiserheatapi-1.7.8/tests/test_rest_payloads.py +73 -0
  22. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/LICENSE +0 -0
  23. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/boiler_interface.py +0 -0
  24. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/button_panel.py +0 -0
  25. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/cli.py +0 -0
  26. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/discovery.py +0 -0
  27. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/exceptions.py +0 -0
  28. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/heating.py +0 -0
  29. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/heating_actuator.py +0 -0
  30. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/helpers/__init__.py +0 -0
  31. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/helpers/automations.py +0 -0
  32. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/helpers/battery.py +0 -0
  33. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/helpers/capabilities.py +0 -0
  34. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/helpers/cloud.py +0 -0
  35. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/helpers/device.py +0 -0
  36. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/helpers/extra_config.py +0 -0
  37. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/helpers/firmware.py +0 -0
  38. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/helpers/gps.py +0 -0
  39. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/helpers/misc.py +0 -0
  40. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/helpers/network.py +0 -0
  41. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/helpers/opentherm.py +0 -0
  42. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/helpers/signal.py +0 -0
  43. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/helpers/special_times.py +0 -0
  44. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/helpers/status.py +0 -0
  45. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/helpers/temp.py +0 -0
  46. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/helpers/threshold.py +0 -0
  47. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/helpers/uiconfiguration.py +0 -0
  48. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/helpers/version.py +0 -0
  49. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/helpers/zigbee.py +0 -0
  50. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/hot_water.py +0 -0
  51. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/light.py +0 -0
  52. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/moments.py +0 -0
  53. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/ptc.py +0 -0
  54. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/pte.py +0 -0
  55. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/refactor.py +0 -0
  56. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/room.py +0 -0
  57. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/roomstat.py +0 -0
  58. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/schedule.py +0 -0
  59. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/shutter.py +0 -0
  60. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/smartplug.py +0 -0
  61. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/smartvalve.py +0 -0
  62. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/system.py +0 -0
  63. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/temp_humidity.py +0 -0
  64. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI/ufh.py +0 -0
  65. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI.egg-info/dependency_links.txt +0 -0
  66. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI.egg-info/entry_points.txt +0 -0
  67. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI.egg-info/requires.txt +0 -0
  68. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/aioWiserHeatAPI.egg-info/top_level.txt +0 -0
  69. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/setup.cfg +0 -0
  70. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/setup.py +0 -0
  71. {aiowiserheatapi-1.7.7 → aiowiserheatapi-1.7.8}/tests/test_schedule.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aioWiserHeatAPI
3
- Version: 1.7.7
3
+ Version: 1.7.8
4
4
  Summary: An AsyncIO API for controlling the Drayton Wiser Heating system
5
5
  Home-page: https://github.com/msp1974/aioWiserHeatAPI
6
6
  Author: Mark Parker
@@ -28,7 +28,7 @@ Dynamic: requires-dist
28
28
  Dynamic: requires-python
29
29
  Dynamic: summary
30
30
 
31
- # Drayton Wiser Hub API Async v1.7.7
31
+ # Drayton Wiser Hub API Async v1.7.8
32
32
 
33
33
  This repository contains a simple API which queries the Drayton Wiser Heating sysystem used in the UK.
34
34
 
@@ -86,6 +86,17 @@ Documentation available in [info.md](https://github.com/msp1974/wiserHeatAPIv2/b
86
86
 
87
87
  ## Changelog
88
88
 
89
+ ### v1.7.8
90
+
91
+ - Added: Standalone equipment collection through `devices.equipments`, including equipment lookup, available control states, overview schedule IDs, and equipment/EVSE rated power values
92
+ - Added: Create, update, enable, disable, and delete native equipment under-power and over-power notifications
93
+ - Added: Enable or disable native smoke alarm notifications and configure window/door sensor notification modes
94
+ - Added: Hub automation collection through `automations`, with cached automation details (triggers, actions, and time constraints), enable/disable controls, notification controls, and deletion
95
+ - Fix: Retry requests over HTTPS when the hub redirects HTTP traffic or explicitly reports that HTTPS is required, while preserving the configured host and port mapping
96
+ - Fix: Send JSON command payloads containing `false`, `0`, or empty values instead of omitting them
97
+ - Fix: Accept successful EVSE commands with empty responses and preserve existing equipment telemetry when responses contain only changed fields
98
+ - Fix: Correct over-power notification data and preserve zero values, disabled states, and missing settings in partial notification records
99
+
89
100
  ### v1.7.7
90
101
 
91
102
  - Fix: Avoid eager `aiofiles.os` imports during startup to prevent mixed-version import failures in Home Assistant (issue #763)
@@ -1,4 +1,4 @@
1
- # Drayton Wiser Hub API Async v1.7.7
1
+ # Drayton Wiser Hub API Async v1.7.8
2
2
 
3
3
  This repository contains a simple API which queries the Drayton Wiser Heating sysystem used in the UK.
4
4
 
@@ -56,6 +56,17 @@ Documentation available in [info.md](https://github.com/msp1974/wiserHeatAPIv2/b
56
56
 
57
57
  ## Changelog
58
58
 
59
+ ### v1.7.8
60
+
61
+ - Added: Standalone equipment collection through `devices.equipments`, including equipment lookup, available control states, overview schedule IDs, and equipment/EVSE rated power values
62
+ - Added: Create, update, enable, disable, and delete native equipment under-power and over-power notifications
63
+ - Added: Enable or disable native smoke alarm notifications and configure window/door sensor notification modes
64
+ - Added: Hub automation collection through `automations`, with cached automation details (triggers, actions, and time constraints), enable/disable controls, notification controls, and deletion
65
+ - Fix: Retry requests over HTTPS when the hub redirects HTTP traffic or explicitly reports that HTTPS is required, while preserving the configured host and port mapping
66
+ - Fix: Send JSON command payloads containing `false`, `0`, or empty values instead of omitting them
67
+ - Fix: Accept successful EVSE commands with empty responses and preserve existing equipment telemetry when responses contain only changed fields
68
+ - Fix: Correct over-power notification data and preserve zero values, disabled states, and missing settings in partial notification records
69
+
59
70
  ### v1.7.7
60
71
 
61
72
  - Fix: Avoid eager `aiofiles.os` imports during startup to prevent mixed-version import failures in Home Assistant (issue #763)
@@ -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.8"
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"""
@@ -93,6 +93,7 @@ WISERHUBSCHEDULES = WISERHUBURL + "schedules/"
93
93
  WISERHUBOPENTHERM = WISERHUBURL + "opentherm/"
94
94
  WISERHUBOPENTHERMV2 = WISERHUBURL + "openTherm/"
95
95
  WISERHUBSTATUS = WISERHUBURL + "status/"
96
+ WISERHUBAUTOMATION = "{}:{}/Automation/"
96
97
  WISERSYSTEM = "System"
97
98
  WISERDEVICE = "Device/{}"
98
99
  WISERHOTWATER = "HotWater/{}"
@@ -113,6 +114,7 @@ WISERBUTTONPANEL = "ButtonPanel/{}"
113
114
  WISEREQUIPMENT = "Equipment/{}"
114
115
  WISERTHRESHOLDSENSOR = "ThresholdSensor/{}"
115
116
  WISERUICONFIGURATION = "UIConfiguration/{}"
117
+ WISERAUTOMATION = "Automation/{}"
116
118
 
117
119
 
118
120
  # Enums
@@ -131,6 +133,15 @@ class WiserTempLimitsEnum(enum.Enum):
131
133
  floorHeatingOffset = {"min": -9, "max": 9, "type": "range"}
132
134
 
133
135
 
136
+ class WiserWindowDoorEnableNotificationEnum(enum.Enum):
137
+ """Native notification modes supported by window and door sensors."""
138
+
139
+ oneither = "OnEither"
140
+ none = "None"
141
+ ontrue = "OnTrue"
142
+ onfalse = "OnFalse"
143
+
144
+
134
145
  class WiserAwayActionEnum(enum.Enum):
135
146
  off = TEXT_OFF
136
147
  nochange = TEXT_NO_CHANGE
@@ -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."""
@@ -47,14 +47,14 @@ class _WiserEquipmentUnderPowerNotificationInfo(object):
47
47
  self._data = data
48
48
 
49
49
  @property
50
- def period_mins(self) -> int:
51
- """Get delivered power"""
52
- return self._data.get("PeriodMins", 0)
50
+ def period_mins(self) -> int | None:
51
+ """Get notification period in minutes."""
52
+ return self._data.get("PeriodMins")
53
53
 
54
54
  @property
55
- def limit(self) -> int:
55
+ def limit(self) -> int | None:
56
56
  """Get limit"""
57
- return self._data.get("Limit", 0)
57
+ return self._data.get("Limit")
58
58
 
59
59
  @property
60
60
  def enabled(self) -> bool:
@@ -68,19 +68,19 @@ class _WiserEquipmentOverPowerNotificationInfo:
68
68
  self._equipment_instance = equipment_instance
69
69
 
70
70
  @property
71
- def period_mins(self) -> int:
71
+ def period_mins(self) -> int | None:
72
72
  """Get period in minutes"""
73
- return self._data.get("PeriodMins") if self._data.get("PeriodMins") else None
73
+ return self._data.get("PeriodMins")
74
74
 
75
75
  @property
76
- def limit(self) -> int:
76
+ def limit(self) -> int | None:
77
77
  """Get limit"""
78
- return self._data.get("Limit", 100000) if self._data.get("Limit") else None
78
+ return self._data.get("Limit")
79
79
 
80
80
  @property
81
81
  def enabled(self) -> bool:
82
82
  """Get status enable"""
83
- return self._data.get("Enabled", False) if self._data.get("Enabled") else None
83
+ return self._data.get("Enabled", False)
84
84
 
85
85
 
86
86
  class _WiserEquipment:
@@ -189,6 +189,27 @@ class _WiserEquipment:
189
189
  """Get current control mode"""
190
190
  return self._data.get("CurrentControlMode", TEXT_UNKNOWN)
191
191
 
192
+ @property
193
+ def available_control_states(self) -> list[str]:
194
+ """Get the control states available for this equipment"""
195
+ states = self._data.get("AvailableControlState")
196
+ return list(states) if isinstance(states, list) else []
197
+
198
+ @property
199
+ def overview_schedule_id(self) -> int | None:
200
+ """Get the overview schedule ID for this equipment"""
201
+ return self._data.get("OverviewScheduleId")
202
+
203
+ @property
204
+ def stated_power(self) -> int | None:
205
+ """Get the stated power value available for this equipment"""
206
+ return self._data.get("StatedPower")
207
+
208
+ @property
209
+ def cable_stated_power(self) -> int | None:
210
+ """Get the cabled stated power value available for this EVSE equipment"""
211
+ return self._data.get("CableStatedPower")
212
+
192
213
  @property
193
214
  def load_state_status(self) -> str:
194
215
  """Get load state status"""
@@ -252,7 +273,9 @@ class _WiserEquipment:
252
273
  # Add for a notification feature Notification if the Power is under or over a threshold
253
274
  # for a time (periodmins)...
254
275
  @property
255
- def under_power_notification(self) -> _WiserEquipmentUnderPowerNotificationInfo:
276
+ def under_power_notification(
277
+ self,
278
+ ) -> _WiserEquipmentUnderPowerNotificationInfo | None:
256
279
  """Get notification info"""
257
280
  return (
258
281
  _WiserEquipmentUnderPowerNotificationInfo(
@@ -263,10 +286,13 @@ class _WiserEquipment:
263
286
  )
264
287
 
265
288
  @property
266
- def over_power_notification(self) -> _WiserEquipmentOverPowerNotificationInfo:
289
+ def over_power_notification(
290
+ self,
291
+ ) -> _WiserEquipmentOverPowerNotificationInfo | None:
267
292
  """Get notification info"""
268
293
  return (
269
- _WiserEquipmentUnderPowerNotificationInfo(
294
+ _WiserEquipmentOverPowerNotificationInfo(
295
+ self,
270
296
  self._data.get("OverPowerNotification")
271
297
  )
272
298
  if self._data.get("OverPowerNotification")