python-openevse-http 0.0.0__py3-none-any.whl
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.
- openevsehttp/__init__.py +58 -0
- openevsehttp/__main__.py +4 -0
- openevsehttp/client.py +473 -0
- openevsehttp/commands.py +511 -0
- openevsehttp/const.py +64 -0
- openevsehttp/exceptions.py +33 -0
- openevsehttp/managers.py +157 -0
- openevsehttp/properties.py +527 -0
- openevsehttp/sensors.py +137 -0
- openevsehttp/utils.py +29 -0
- openevsehttp/websocket.py +275 -0
- python_openevse_http-0.0.0.dist-info/METADATA +121 -0
- python_openevse_http-0.0.0.dist-info/RECORD +16 -0
- python_openevse_http-0.0.0.dist-info/WHEEL +5 -0
- python_openevse_http-0.0.0.dist-info/licenses/LICENSE +201 -0
- python_openevse_http-0.0.0.dist-info/top_level.txt +1 -0
openevsehttp/managers.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Manager methods (limits, claims) for the OpenEVSE charger."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from .const import CLIENT, RELEASE, TYPE, VALUE
|
|
10
|
+
from .exceptions import InvalidType, UnsupportedFeature
|
|
11
|
+
|
|
12
|
+
_LOGGER = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ManagersMixin:
|
|
16
|
+
"""Mixin providing limit and claim management methods for OpenEVSE."""
|
|
17
|
+
|
|
18
|
+
url: str
|
|
19
|
+
|
|
20
|
+
# These are defined in client.py
|
|
21
|
+
def _version_check(self, min_version: str, max_version: str = "") -> bool:
|
|
22
|
+
raise NotImplementedError
|
|
23
|
+
|
|
24
|
+
async def process_request(
|
|
25
|
+
self, url: str, method: str = "", data: Any = None, rapi: Any = None
|
|
26
|
+
) -> Mapping[str, Any] | list[Any] | str:
|
|
27
|
+
raise NotImplementedError
|
|
28
|
+
|
|
29
|
+
def _normalize_response(self, response: Any) -> dict[str, Any] | list[Any]:
|
|
30
|
+
"""Normalize response to a dict or list."""
|
|
31
|
+
raise NotImplementedError
|
|
32
|
+
|
|
33
|
+
# Limit endpoint
|
|
34
|
+
async def set_limit(
|
|
35
|
+
self, limit_type: str, value: int, release: bool | None = None
|
|
36
|
+
) -> Any:
|
|
37
|
+
"""Set charge limit."""
|
|
38
|
+
if not self._version_check("5.0.0"):
|
|
39
|
+
_LOGGER.debug("Feature not supported for older firmware.")
|
|
40
|
+
raise UnsupportedFeature
|
|
41
|
+
|
|
42
|
+
valid_types = ["time", "energy", "soc", "range"]
|
|
43
|
+
|
|
44
|
+
if limit_type not in valid_types:
|
|
45
|
+
raise InvalidType
|
|
46
|
+
|
|
47
|
+
url = f"{self.url}limit"
|
|
48
|
+
fetched_data = await self.get_limit()
|
|
49
|
+
|
|
50
|
+
data: dict[str, Any] = {}
|
|
51
|
+
if isinstance(fetched_data, Mapping):
|
|
52
|
+
for key in (TYPE, VALUE, RELEASE):
|
|
53
|
+
if key in fetched_data:
|
|
54
|
+
data[key] = fetched_data[key]
|
|
55
|
+
|
|
56
|
+
data[TYPE] = limit_type
|
|
57
|
+
data[VALUE] = value
|
|
58
|
+
if release is not None:
|
|
59
|
+
data[RELEASE] = release
|
|
60
|
+
|
|
61
|
+
_LOGGER.debug("Limit data: %s", data)
|
|
62
|
+
_LOGGER.debug("Setting limit config on %s", url)
|
|
63
|
+
response = await self.process_request(url=url, method="post", data=data)
|
|
64
|
+
return self._normalize_response(response)
|
|
65
|
+
|
|
66
|
+
async def clear_limit(self) -> Any:
|
|
67
|
+
"""Clear charge limit."""
|
|
68
|
+
if not self._version_check("5.0.0"):
|
|
69
|
+
_LOGGER.debug("Feature not supported for older firmware.")
|
|
70
|
+
raise UnsupportedFeature
|
|
71
|
+
|
|
72
|
+
url = f"{self.url}limit"
|
|
73
|
+
|
|
74
|
+
_LOGGER.debug("Clearing limit config on %s", url)
|
|
75
|
+
response = await self.process_request(url=url, method="delete")
|
|
76
|
+
return self._normalize_response(response)
|
|
77
|
+
|
|
78
|
+
async def get_limit(self) -> Any:
|
|
79
|
+
"""Get charge limit."""
|
|
80
|
+
if not self._version_check("5.0.0"):
|
|
81
|
+
_LOGGER.debug("Feature not supported for older firmware.")
|
|
82
|
+
raise UnsupportedFeature
|
|
83
|
+
|
|
84
|
+
url = f"{self.url}limit"
|
|
85
|
+
|
|
86
|
+
_LOGGER.debug("Getting limit config on %s", url)
|
|
87
|
+
response = await self.process_request(url=url, method="get")
|
|
88
|
+
return self._normalize_response(response)
|
|
89
|
+
|
|
90
|
+
async def make_claim(
|
|
91
|
+
self,
|
|
92
|
+
state: str | None = None,
|
|
93
|
+
charge_current: int | None = None,
|
|
94
|
+
max_current: int | None = None,
|
|
95
|
+
auto_release: bool = True,
|
|
96
|
+
client: int = CLIENT,
|
|
97
|
+
) -> Any:
|
|
98
|
+
"""Make a claim."""
|
|
99
|
+
if not self._version_check("4.1.0"):
|
|
100
|
+
_LOGGER.debug("Feature not supported for older firmware.")
|
|
101
|
+
raise UnsupportedFeature
|
|
102
|
+
|
|
103
|
+
if state not in ["active", "disabled", None]:
|
|
104
|
+
_LOGGER.error("Invalid claim state: %s", state)
|
|
105
|
+
raise ValueError(
|
|
106
|
+
f"Invalid claim state: {state}. Allowed: ['active', 'disabled', None]"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
url = f"{self.url}claims/{client}"
|
|
110
|
+
|
|
111
|
+
data: dict[str, Any] = {}
|
|
112
|
+
|
|
113
|
+
data["auto_release"] = auto_release
|
|
114
|
+
|
|
115
|
+
if state is not None:
|
|
116
|
+
data["state"] = state
|
|
117
|
+
if charge_current is not None:
|
|
118
|
+
data["charge_current"] = charge_current
|
|
119
|
+
if max_current is not None:
|
|
120
|
+
data["max_current"] = max_current
|
|
121
|
+
|
|
122
|
+
_LOGGER.debug("Claim data: %s", data)
|
|
123
|
+
_LOGGER.debug("Setting up claim on %s", url)
|
|
124
|
+
response = await self.process_request(url=url, method="post", data=data)
|
|
125
|
+
return self._normalize_response(response)
|
|
126
|
+
|
|
127
|
+
async def release_claim(self, client: int = CLIENT) -> Any:
|
|
128
|
+
"""Delete a claim."""
|
|
129
|
+
if not self._version_check("4.1.0"):
|
|
130
|
+
_LOGGER.debug("Feature not supported for older firmware.")
|
|
131
|
+
raise UnsupportedFeature
|
|
132
|
+
|
|
133
|
+
url = f"{self.url}claims/{client}"
|
|
134
|
+
|
|
135
|
+
_LOGGER.debug("Releasing claim on %s", url)
|
|
136
|
+
response = await self.process_request(url=url, method="delete")
|
|
137
|
+
return self._normalize_response(response)
|
|
138
|
+
|
|
139
|
+
async def list_claims(self, target: bool | None = None) -> Any:
|
|
140
|
+
"""List all claims.
|
|
141
|
+
|
|
142
|
+
:param target: If True, returns the target (active) claim.
|
|
143
|
+
If False or None (default), returns a list of all claims.
|
|
144
|
+
"""
|
|
145
|
+
if not self._version_check("4.1.0"):
|
|
146
|
+
_LOGGER.debug("Feature not supported for older firmware.")
|
|
147
|
+
raise UnsupportedFeature
|
|
148
|
+
|
|
149
|
+
target_check = ""
|
|
150
|
+
if target is True:
|
|
151
|
+
target_check = "/target"
|
|
152
|
+
|
|
153
|
+
url = f"{self.url}claims{target_check}"
|
|
154
|
+
|
|
155
|
+
_LOGGER.debug("Getting claims on %s", url)
|
|
156
|
+
response = await self.process_request(url=url, method="get")
|
|
157
|
+
return self._normalize_response(response)
|
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
"""Property accessors for the OpenEVSE charger."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from datetime import datetime, timedelta, timezone
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from .const import MAX_AMPS, MIN_AMPS, states
|
|
11
|
+
from .exceptions import UnsupportedFeature
|
|
12
|
+
from .utils import normalize_version
|
|
13
|
+
|
|
14
|
+
_LOGGER = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PropertiesMixin:
|
|
18
|
+
"""Mixin providing all @property accessors for OpenEVSE."""
|
|
19
|
+
|
|
20
|
+
_status: dict
|
|
21
|
+
_config: dict
|
|
22
|
+
|
|
23
|
+
# These are used by properties but defined in client.py
|
|
24
|
+
def _version_check(self, min_version: str, max_version: str = "") -> bool:
|
|
25
|
+
raise NotImplementedError
|
|
26
|
+
|
|
27
|
+
async def list_claims(self, target: bool | None = None) -> Any:
|
|
28
|
+
raise NotImplementedError
|
|
29
|
+
|
|
30
|
+
async def get_override(self) -> Mapping[str, Any] | list[Any]:
|
|
31
|
+
raise NotImplementedError
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def led_brightness(self) -> int | None:
|
|
35
|
+
"""Return charger led_brightness."""
|
|
36
|
+
if not self._version_check("4.1.0"):
|
|
37
|
+
_LOGGER.debug("Feature not supported for older firmware.")
|
|
38
|
+
raise UnsupportedFeature
|
|
39
|
+
return self._config.get("led_brightness")
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def hostname(self) -> str | None:
|
|
43
|
+
"""Return charger hostname."""
|
|
44
|
+
return self._config.get("hostname")
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def wifi_ssid(self) -> str | None:
|
|
48
|
+
"""Return charger connected SSID."""
|
|
49
|
+
return self._config.get("ssid")
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def ammeter_offset(self) -> int | None:
|
|
53
|
+
"""Return ammeter's current offset."""
|
|
54
|
+
return self._config.get("offset")
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def ammeter_scale_factor(self) -> int | None:
|
|
58
|
+
"""Return ammeter's current scale factor."""
|
|
59
|
+
return self._config.get("scale")
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def temp_check_enabled(self) -> bool:
|
|
63
|
+
"""Return True if enabled, False if disabled."""
|
|
64
|
+
return bool(self._config.get("tempt", False))
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def diode_check_enabled(self) -> bool:
|
|
68
|
+
"""Return True if enabled, False if disabled."""
|
|
69
|
+
return bool(self._config.get("diodet", False))
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def vent_required_enabled(self) -> bool:
|
|
73
|
+
"""Return True if enabled, False if disabled."""
|
|
74
|
+
return bool(self._config.get("ventt", False))
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def ground_check_enabled(self) -> bool:
|
|
78
|
+
"""Return True if enabled, False if disabled."""
|
|
79
|
+
return bool(self._config.get("groundt", False))
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def stuck_relay_check_enabled(self) -> bool:
|
|
83
|
+
"""Return True if enabled, False if disabled."""
|
|
84
|
+
return bool(self._config.get("relayt", False))
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def service_level(self) -> int | str | None:
|
|
88
|
+
"""Return the service level (1, 2, or 'A')."""
|
|
89
|
+
return self._config.get("service")
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def openevse_firmware(self) -> str | None:
|
|
93
|
+
"""Return the firmware version."""
|
|
94
|
+
return self._config.get("firmware")
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def max_current_soft(self) -> int | None:
|
|
98
|
+
"""Return the max current soft."""
|
|
99
|
+
if "max_current_soft" in self._config:
|
|
100
|
+
return self._config.get("max_current_soft")
|
|
101
|
+
return self._status.get("pilot")
|
|
102
|
+
|
|
103
|
+
async def get_charge_current(self) -> int | None:
|
|
104
|
+
"""Get the charge current."""
|
|
105
|
+
try:
|
|
106
|
+
claims_data = await self.list_claims(target=True)
|
|
107
|
+
claims = claims_data if isinstance(claims_data, list) else [claims_data]
|
|
108
|
+
for claim in claims:
|
|
109
|
+
if isinstance(claim, dict):
|
|
110
|
+
properties = claim.get("properties")
|
|
111
|
+
if isinstance(properties, dict) and "charge_current" in properties:
|
|
112
|
+
try:
|
|
113
|
+
charge_current = int(properties["charge_current"])
|
|
114
|
+
max_hard = int(self._config.get("max_current_hard", 48))
|
|
115
|
+
return min(charge_current, max_hard)
|
|
116
|
+
except (TypeError, ValueError):
|
|
117
|
+
pass
|
|
118
|
+
except (UnsupportedFeature, IndexError, KeyError):
|
|
119
|
+
pass
|
|
120
|
+
|
|
121
|
+
if "max_current_soft" in self._config:
|
|
122
|
+
return self._config.get("max_current_soft")
|
|
123
|
+
return self._status.get("pilot")
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def max_current(self) -> int | None:
|
|
127
|
+
"""Return the max current."""
|
|
128
|
+
return self._status.get("max_current", None)
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def wifi_firmware(self) -> str | None:
|
|
132
|
+
"""Return the ESP firmware version."""
|
|
133
|
+
value = self._config.get("version")
|
|
134
|
+
if value is not None:
|
|
135
|
+
value = normalize_version(value)
|
|
136
|
+
return value
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def ip_address(self) -> str | None:
|
|
140
|
+
"""Return the ip address."""
|
|
141
|
+
return self._status.get("ipaddress")
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def charging_voltage(self) -> int | None:
|
|
145
|
+
"""Return the charging voltage."""
|
|
146
|
+
return self._status.get("voltage")
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def mode(self) -> str | None:
|
|
150
|
+
"""Return the mode."""
|
|
151
|
+
return self._status.get("mode")
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def using_ethernet(self) -> bool:
|
|
155
|
+
"""Return True if enabled, False if disabled."""
|
|
156
|
+
return bool(self._status.get("eth_connected", False))
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def stuck_relay_trip_count(self) -> int | None:
|
|
160
|
+
"""Return the stuck relay count."""
|
|
161
|
+
return self._status.get("stuckcount")
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def no_gnd_trip_count(self) -> int | None:
|
|
165
|
+
"""Return the no ground count."""
|
|
166
|
+
return self._status.get("nogndcount")
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def gfi_trip_count(self) -> int | None:
|
|
170
|
+
"""Return the GFCI count."""
|
|
171
|
+
return self._status.get("gfcicount")
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def status(self) -> str:
|
|
175
|
+
"""Return charger's state."""
|
|
176
|
+
# Check if "status" is already a non-null string in _status (some versions)
|
|
177
|
+
val = self._status.get("status")
|
|
178
|
+
if val is not None:
|
|
179
|
+
return str(val)
|
|
180
|
+
|
|
181
|
+
# Fall back to state mapping
|
|
182
|
+
return self.state
|
|
183
|
+
|
|
184
|
+
@property
|
|
185
|
+
def state(self) -> str:
|
|
186
|
+
"""Return charger's state."""
|
|
187
|
+
try:
|
|
188
|
+
code = int(self._status.get("state", 0))
|
|
189
|
+
except (ValueError, TypeError):
|
|
190
|
+
code = 0
|
|
191
|
+
return states.get(code, "unknown")
|
|
192
|
+
|
|
193
|
+
@property
|
|
194
|
+
def state_raw(self) -> int | None:
|
|
195
|
+
"""Return charger's state int form."""
|
|
196
|
+
return self._status.get("state")
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def charge_time_elapsed(self) -> int | None:
|
|
200
|
+
"""Return elapsed charging time."""
|
|
201
|
+
return self._status.get("elapsed")
|
|
202
|
+
|
|
203
|
+
@property
|
|
204
|
+
def wifi_signal(self) -> int | None:
|
|
205
|
+
"""Return charger's wifi signal."""
|
|
206
|
+
return self._status.get("srssi")
|
|
207
|
+
|
|
208
|
+
@property
|
|
209
|
+
def charging_current(self) -> float | None:
|
|
210
|
+
"""Return the charge current.
|
|
211
|
+
|
|
212
|
+
0 if is not currently charging.
|
|
213
|
+
"""
|
|
214
|
+
return self._status.get("amp")
|
|
215
|
+
|
|
216
|
+
@property
|
|
217
|
+
def current_capacity(self) -> int | None:
|
|
218
|
+
"""Return the current capacity."""
|
|
219
|
+
return self._status.get("pilot")
|
|
220
|
+
|
|
221
|
+
@property
|
|
222
|
+
def usage_total(self) -> float | None:
|
|
223
|
+
"""Return the total energy usage in Wh."""
|
|
224
|
+
if "total_energy" in self._status:
|
|
225
|
+
return self._status.get("total_energy")
|
|
226
|
+
return self._status.get("watthour")
|
|
227
|
+
|
|
228
|
+
@property
|
|
229
|
+
def ambient_temperature(self) -> float | None:
|
|
230
|
+
"""Return the temperature of the ambient sensor, in degrees Celsius."""
|
|
231
|
+
for key in ["temp", "temp1"]:
|
|
232
|
+
temp = self._status.get(key)
|
|
233
|
+
if temp is not None:
|
|
234
|
+
try:
|
|
235
|
+
return float(temp) / 10
|
|
236
|
+
except (ValueError, TypeError):
|
|
237
|
+
continue
|
|
238
|
+
return None
|
|
239
|
+
|
|
240
|
+
@property
|
|
241
|
+
def rtc_temperature(self) -> float | None:
|
|
242
|
+
"""Return the temperature of the real time clock sensor."""
|
|
243
|
+
temp = self._status.get("temp2")
|
|
244
|
+
try:
|
|
245
|
+
if temp is None or isinstance(temp, bool):
|
|
246
|
+
return None
|
|
247
|
+
return float(temp) / 10
|
|
248
|
+
except (ValueError, TypeError):
|
|
249
|
+
return None
|
|
250
|
+
|
|
251
|
+
@property
|
|
252
|
+
def ir_temperature(self) -> float | None:
|
|
253
|
+
"""Return the temperature of the IR remote sensor.
|
|
254
|
+
|
|
255
|
+
In degrees Celsius.
|
|
256
|
+
"""
|
|
257
|
+
temp = self._status.get("temp3")
|
|
258
|
+
try:
|
|
259
|
+
if temp is None or isinstance(temp, bool):
|
|
260
|
+
return None
|
|
261
|
+
return float(temp) / 10
|
|
262
|
+
except (ValueError, TypeError):
|
|
263
|
+
return None
|
|
264
|
+
|
|
265
|
+
@property
|
|
266
|
+
def esp_temperature(self) -> float | None:
|
|
267
|
+
"""Return the temperature of the ESP sensor, in degrees Celsius."""
|
|
268
|
+
temp = self._status.get("temp4")
|
|
269
|
+
try:
|
|
270
|
+
if temp is None or isinstance(temp, bool):
|
|
271
|
+
return None
|
|
272
|
+
return float(temp) / 10
|
|
273
|
+
except (ValueError, TypeError):
|
|
274
|
+
return None
|
|
275
|
+
|
|
276
|
+
@property
|
|
277
|
+
def time(self) -> datetime | None:
|
|
278
|
+
"""Get the RTC time."""
|
|
279
|
+
value = self._status.get("time")
|
|
280
|
+
if value:
|
|
281
|
+
try:
|
|
282
|
+
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
283
|
+
except (ValueError, AttributeError):
|
|
284
|
+
return None
|
|
285
|
+
return None
|
|
286
|
+
|
|
287
|
+
@property
|
|
288
|
+
def usage_session(self) -> float | None:
|
|
289
|
+
"""Get the energy usage for the current charging session.
|
|
290
|
+
|
|
291
|
+
Return the energy usage in Wh.
|
|
292
|
+
"""
|
|
293
|
+
if "session_energy" in self._status:
|
|
294
|
+
return self._status.get("session_energy")
|
|
295
|
+
wattsec = self._status.get("wattsec")
|
|
296
|
+
if wattsec is not None:
|
|
297
|
+
try:
|
|
298
|
+
return float(round(float(wattsec) / 3600, 2))
|
|
299
|
+
except (ValueError, TypeError):
|
|
300
|
+
return None
|
|
301
|
+
return None
|
|
302
|
+
|
|
303
|
+
@property
|
|
304
|
+
def total_day(self) -> float | None:
|
|
305
|
+
"""Get the total day energy usage."""
|
|
306
|
+
return self._status.get("total_day", None)
|
|
307
|
+
|
|
308
|
+
@property
|
|
309
|
+
def total_week(self) -> float | None:
|
|
310
|
+
"""Get the total week energy usage."""
|
|
311
|
+
return self._status.get("total_week", None)
|
|
312
|
+
|
|
313
|
+
@property
|
|
314
|
+
def total_month(self) -> float | None:
|
|
315
|
+
"""Get the total month energy usage."""
|
|
316
|
+
return self._status.get("total_month", None)
|
|
317
|
+
|
|
318
|
+
@property
|
|
319
|
+
def total_year(self) -> float | None:
|
|
320
|
+
"""Get the total year energy usage."""
|
|
321
|
+
return self._status.get("total_year", None)
|
|
322
|
+
|
|
323
|
+
@property
|
|
324
|
+
def has_limit(self) -> bool | None:
|
|
325
|
+
"""Return if a limit has been set."""
|
|
326
|
+
return self._status.get("has_limit", self._status.get("limit", None))
|
|
327
|
+
|
|
328
|
+
@property
|
|
329
|
+
def protocol_version(self) -> str | None:
|
|
330
|
+
"""Return the protocol version."""
|
|
331
|
+
protocol = self._config.get("protocol")
|
|
332
|
+
if protocol == "-":
|
|
333
|
+
return None
|
|
334
|
+
return protocol
|
|
335
|
+
|
|
336
|
+
@property
|
|
337
|
+
def vehicle(self) -> bool:
|
|
338
|
+
"""Return if a vehicle is connected to the EVSE."""
|
|
339
|
+
return self._status.get("vehicle", False)
|
|
340
|
+
|
|
341
|
+
@property
|
|
342
|
+
def ota_update(self) -> bool:
|
|
343
|
+
"""Return if an OTA update is active."""
|
|
344
|
+
return self._status.get("ota_update", False)
|
|
345
|
+
|
|
346
|
+
@property
|
|
347
|
+
def manual_override(self) -> bool:
|
|
348
|
+
"""Return if Manual Override is set."""
|
|
349
|
+
return self._status.get("manual_override", False)
|
|
350
|
+
|
|
351
|
+
@property
|
|
352
|
+
def divertmode(self) -> str:
|
|
353
|
+
"""Return the divert mode."""
|
|
354
|
+
mode = self._status.get("divertmode", 1)
|
|
355
|
+
if mode == 1:
|
|
356
|
+
return "fast"
|
|
357
|
+
return "eco"
|
|
358
|
+
|
|
359
|
+
@property
|
|
360
|
+
def charge_mode(self) -> str | None:
|
|
361
|
+
"""Return the charge mode."""
|
|
362
|
+
return self._config.get("charge_mode")
|
|
363
|
+
|
|
364
|
+
@property
|
|
365
|
+
def available_current(self) -> float | None:
|
|
366
|
+
"""Return the computed available current for divert."""
|
|
367
|
+
return self._status.get("available_current")
|
|
368
|
+
|
|
369
|
+
@property
|
|
370
|
+
def smoothed_available_current(self) -> float | None:
|
|
371
|
+
"""Return the computed smoothed available current for divert."""
|
|
372
|
+
return self._status.get("smoothed_available_current")
|
|
373
|
+
|
|
374
|
+
@property
|
|
375
|
+
def charge_rate(self) -> float | None:
|
|
376
|
+
"""Return the divert charge rate."""
|
|
377
|
+
return self._status.get("charge_rate")
|
|
378
|
+
|
|
379
|
+
@property
|
|
380
|
+
def divert_active(self) -> bool:
|
|
381
|
+
"""Return if divert is active."""
|
|
382
|
+
return bool(self._config.get("divert_enabled", False))
|
|
383
|
+
|
|
384
|
+
@property
|
|
385
|
+
def wifi_serial(self) -> str | None:
|
|
386
|
+
"""Return wifi serial."""
|
|
387
|
+
return self._config.get("wifi_serial", None)
|
|
388
|
+
|
|
389
|
+
@property
|
|
390
|
+
def charging_power(self) -> float | None:
|
|
391
|
+
"""Return the charge power.
|
|
392
|
+
|
|
393
|
+
Calculate Watts base on V*I
|
|
394
|
+
"""
|
|
395
|
+
if self._status is not None and all(
|
|
396
|
+
key in self._status for key in ["voltage", "amp"]
|
|
397
|
+
):
|
|
398
|
+
voltage = self._status["voltage"]
|
|
399
|
+
amp = self._status["amp"]
|
|
400
|
+
if isinstance(voltage, int | float) and isinstance(amp, int | float):
|
|
401
|
+
return round(voltage * amp, 2)
|
|
402
|
+
return None
|
|
403
|
+
|
|
404
|
+
# Shaper values
|
|
405
|
+
@property
|
|
406
|
+
def shaper_active(self) -> bool | None:
|
|
407
|
+
"""Return if shaper is active."""
|
|
408
|
+
return self._status.get("shaper", None)
|
|
409
|
+
|
|
410
|
+
@property
|
|
411
|
+
def shaper_live_power(self) -> int | None:
|
|
412
|
+
"""Return shaper live power reading."""
|
|
413
|
+
return self._status.get("shaper_live_pwr", None)
|
|
414
|
+
|
|
415
|
+
@property
|
|
416
|
+
def shaper_available_current(self) -> float | None:
|
|
417
|
+
"""Return shaper available current."""
|
|
418
|
+
shaper_cur = self._status.get("shaper_cur")
|
|
419
|
+
if shaper_cur == 255:
|
|
420
|
+
return self._status.get("pilot")
|
|
421
|
+
return shaper_cur
|
|
422
|
+
|
|
423
|
+
@property
|
|
424
|
+
def shaper_max_power(self) -> int | None:
|
|
425
|
+
"""Return shaper live power reading."""
|
|
426
|
+
return self._status.get("shaper_max_pwr", None)
|
|
427
|
+
|
|
428
|
+
@property
|
|
429
|
+
def shaper_updated(self) -> bool:
|
|
430
|
+
"""Return shaper updated boolean."""
|
|
431
|
+
return bool(self._status.get("shaper_updated", False))
|
|
432
|
+
|
|
433
|
+
# Vehicle values
|
|
434
|
+
@property
|
|
435
|
+
def vehicle_soc(self) -> int | None:
|
|
436
|
+
"""Return battery level."""
|
|
437
|
+
return self._status.get("vehicle_soc", self._status.get("battery_level", None))
|
|
438
|
+
|
|
439
|
+
@property
|
|
440
|
+
def vehicle_range(self) -> int | None:
|
|
441
|
+
"""Return battery range."""
|
|
442
|
+
return self._status.get(
|
|
443
|
+
"vehicle_range", self._status.get("battery_range", None)
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
@property
|
|
447
|
+
def vehicle_eta(self) -> datetime | None:
|
|
448
|
+
"""Return time to full charge."""
|
|
449
|
+
value = self._status.get(
|
|
450
|
+
"time_to_full_charge", self._status.get("vehicle_eta", None)
|
|
451
|
+
)
|
|
452
|
+
if value is None or isinstance(value, bool):
|
|
453
|
+
return None
|
|
454
|
+
try:
|
|
455
|
+
seconds = float(value)
|
|
456
|
+
except (TypeError, ValueError):
|
|
457
|
+
return None
|
|
458
|
+
return datetime.now(timezone.utc) + timedelta(seconds=seconds)
|
|
459
|
+
|
|
460
|
+
# There is currently no min/max amps JSON data
|
|
461
|
+
# available via HTTP API methods
|
|
462
|
+
@property
|
|
463
|
+
def min_amps(self) -> int:
|
|
464
|
+
"""Return the minimum amps."""
|
|
465
|
+
return self._config.get("min_current_hard", MIN_AMPS)
|
|
466
|
+
|
|
467
|
+
@property
|
|
468
|
+
def max_amps(self) -> int:
|
|
469
|
+
"""Return the maximum amps."""
|
|
470
|
+
return self._config.get("max_current_hard", MAX_AMPS)
|
|
471
|
+
|
|
472
|
+
@property
|
|
473
|
+
def mqtt_connected(self) -> bool:
|
|
474
|
+
"""Return the status of the mqtt connection."""
|
|
475
|
+
return bool(self._status.get("mqtt_connected", False))
|
|
476
|
+
|
|
477
|
+
@property
|
|
478
|
+
def emoncms_connected(self) -> bool | None:
|
|
479
|
+
"""Return the status of the emoncms connection."""
|
|
480
|
+
return self._status.get("emoncms_connected", None)
|
|
481
|
+
|
|
482
|
+
@property
|
|
483
|
+
def ocpp_connected(self) -> bool | None:
|
|
484
|
+
"""Return the status of the ocpp connection."""
|
|
485
|
+
return self._status.get("ocpp_connected", None)
|
|
486
|
+
|
|
487
|
+
@property
|
|
488
|
+
def uptime(self) -> int | None:
|
|
489
|
+
"""Return the unit uptime."""
|
|
490
|
+
return self._status.get("uptime", None)
|
|
491
|
+
|
|
492
|
+
@property
|
|
493
|
+
def freeram(self) -> int | None:
|
|
494
|
+
"""Return the unit freeram."""
|
|
495
|
+
return self._status.get("freeram", None)
|
|
496
|
+
|
|
497
|
+
# Safety counts
|
|
498
|
+
@property
|
|
499
|
+
def checks_count(self) -> dict:
|
|
500
|
+
"""Return the safety checks counts."""
|
|
501
|
+
attributes = ("gfcicount", "nogndcount", "stuckcount")
|
|
502
|
+
counts = {}
|
|
503
|
+
if self._status is not None and set(attributes).issubset(self._status.keys()):
|
|
504
|
+
counts["gfcicount"] = self._status["gfcicount"]
|
|
505
|
+
counts["nogndcount"] = self._status["nogndcount"]
|
|
506
|
+
counts["stuckcount"] = self._status["stuckcount"]
|
|
507
|
+
return counts
|
|
508
|
+
|
|
509
|
+
async def get_override_state(self) -> str | None:
|
|
510
|
+
"""Get the unit override state."""
|
|
511
|
+
try:
|
|
512
|
+
override = await self.get_override()
|
|
513
|
+
except UnsupportedFeature:
|
|
514
|
+
_LOGGER.debug("Override state unavailable on older firmware.")
|
|
515
|
+
return None
|
|
516
|
+
if isinstance(override, dict):
|
|
517
|
+
state = override.get("state", "auto")
|
|
518
|
+
return "auto" if state is None else state
|
|
519
|
+
return "auto"
|
|
520
|
+
|
|
521
|
+
@property
|
|
522
|
+
def current_power(self) -> int:
|
|
523
|
+
"""Return the current power (live) in watts."""
|
|
524
|
+
if not self._version_check("4.2.2"):
|
|
525
|
+
_LOGGER.debug("Feature not supported for older firmware.")
|
|
526
|
+
raise UnsupportedFeature
|
|
527
|
+
return self._status.get("power", 0)
|