python-openevse-http 1.7.0__py3-none-any.whl → 1.8.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/client.py CHANGED
@@ -26,6 +26,7 @@ from .const import (
26
26
  from .exceptions import (
27
27
  AlreadyListening,
28
28
  AuthenticationError,
29
+ CommandFailedError,
29
30
  MissingMethod,
30
31
  MissingSerial,
31
32
  ParseJSONError,
@@ -186,15 +187,16 @@ class OpenEVSE(CommandsMixin, ManagersMixin, SensorsMixin, PropertiesMixin):
186
187
 
187
188
  if resp.status == 400:
188
189
  if isinstance(response_content, dict) and "msg" in response_content:
189
- _LOGGER.error("Error 400: %s", response_content["msg"])
190
+ msg = str(response_content["msg"])
190
191
  elif (
191
192
  isinstance(response_content, dict)
192
193
  and "error" in response_content
193
194
  ):
194
- _LOGGER.error("Error 400: %s", response_content["error"])
195
+ msg = str(response_content["error"])
195
196
  else:
196
- _LOGGER.error("Error 400: %s", response_content)
197
- raise ParseJSONError
197
+ msg = str(response_content)
198
+ _LOGGER.error("Error 400: %s", msg)
199
+ raise CommandFailedError(msg)
198
200
  if resp.status == 401:
199
201
  _LOGGER.error("Authentication error: %s", response_content)
200
202
  raise AuthenticationError
@@ -271,17 +271,31 @@ class CoreCommandsMixin(BaseCommandMixin):
271
271
 
272
272
  # Strict success check:
273
273
  # 1. Must be a Mapping
274
- # 2. Must have "result" == "OK" OR "success" is True
275
- # 3. Must NOT have an "error" key
276
- # 4. If "msg" is present, it must be "OK" or contain "ok" (case-insensitive)
274
+ # 2. Must NOT have an "error" key
275
+ # 3. Must have "result" in ("OK", "ok", True) OR "success" is True
276
+ # OR "msg" indicating restart acknowledgment ("restart gateway", "ok")
277
277
  success = (
278
278
  isinstance(response, Mapping)
279
- and (response.get("result") == "OK" or response.get("success") is True)
280
279
  and not response.get("error")
280
+ and (
281
+ response.get("result") in ("OK", "ok", True)
282
+ or response.get("success") is True
283
+ or (
284
+ isinstance(response.get("msg"), str)
285
+ and any(
286
+ val in response["msg"].lower()
287
+ for val in ("ok", "restart gateway")
288
+ )
289
+ )
290
+ )
281
291
  )
282
292
  if success and isinstance(response, Mapping) and "msg" in response:
283
293
  msg_val = str(response["msg"]).lower()
284
- if msg_val != "ok" and "ok" not in msg_val:
294
+ if (
295
+ msg_val != "ok"
296
+ and "ok" not in msg_val
297
+ and "restart gateway" not in msg_val
298
+ ):
285
299
  success = False
286
300
 
287
301
  if not success:
@@ -227,3 +227,107 @@ class DiagnosticsCommandsMixin(BaseCommandMixin):
227
227
  raise CommandFailedError(f"Invalid response from /logs: {response}")
228
228
 
229
229
  return dict(response)
230
+
231
+ async def get_notifications(self) -> dict[str, Any]:
232
+ """Retrieve the advisory notifications list from the gateway.
233
+
234
+ Queries 'GET /notifications'.
235
+ Requires gateway firmware 5.1.0 or higher.
236
+
237
+ Expected response shape:
238
+ {
239
+ "count": int,
240
+ "max_severity": "info" | "warning" | "critical" | None,
241
+ "notifications": [
242
+ {
243
+ "id": str,
244
+ "category": str,
245
+ "severity": str,
246
+ "sticky": bool,
247
+ "acked": bool,
248
+ "first_seen": int,
249
+ "last_seen": int,
250
+ },
251
+ ...
252
+ ]
253
+ }
254
+
255
+ :return: Dict containing 'count', 'max_severity', and 'notifications' list.
256
+ :raises UnsupportedFeature: If gateway firmware is older than 5.1.0.
257
+ :raises CommandFailedError: If response is invalid.
258
+ """
259
+ self._require_firmware("5.1.0", "get_notifications")
260
+
261
+ url = f"{self.url}notifications"
262
+ _LOGGER.debug("Querying notifications: %s", url)
263
+ response = await self.process_request(url=url, method="get")
264
+
265
+ if not isinstance(response, Mapping):
266
+ _LOGGER.error("Invalid response from /notifications: %s", response)
267
+ raise CommandFailedError(
268
+ f"Invalid response from /notifications: {response}"
269
+ )
270
+
271
+ return dict(response)
272
+
273
+ async def acknowledge_notification(self, notification_id: str) -> None:
274
+ """Acknowledge (mute) an active advisory notification by ID.
275
+
276
+ Issues 'POST /notifications/ack' with the notification ID.
277
+ Requires gateway firmware 5.1.0 or higher.
278
+
279
+ :param notification_id: The identifier of the advisory (e.g. 'safety.ground_check').
280
+ :raises UnsupportedFeature: If gateway firmware is older than 5.1.0.
281
+ :raises ValueError: If notification_id is empty or not a string.
282
+ :raises CommandFailedError: If the acknowledgment fails or returns an error.
283
+ """
284
+ self._require_firmware("5.1.0", "acknowledge_notification")
285
+
286
+ if not isinstance(notification_id, str) or not notification_id.strip():
287
+ raise ValueError("notification_id must be a non-empty string.")
288
+
289
+ url = f"{self.url}notifications/ack"
290
+ data = {"id": notification_id.strip()}
291
+
292
+ _LOGGER.debug(
293
+ "Acknowledging notification %s at %s", notification_id.strip(), url
294
+ )
295
+ response = await self.process_request(url=url, method="post", data=data)
296
+ response = self._normalize_response(response)
297
+
298
+ msg = response.get("msg") if isinstance(response, Mapping) else response
299
+ if msg not in SUCCESS_ANSWERS:
300
+ _LOGGER.error("Problem acknowledging notification: %s", response)
301
+ raise CommandFailedError(f"Problem acknowledging notification: {response}")
302
+
303
+ async def reset_energy_meter(
304
+ self, hard: bool = False, import_from_evse: bool = False
305
+ ) -> None:
306
+ """Reset the charger energy meter.
307
+
308
+ Issues DELETE /emeter with hard and import flags.
309
+
310
+ :param hard: If True, sets total energy and switch counter to 0 (hard reset).
311
+ If False, performs a soft reset.
312
+ :param import_from_evse: If True, allows re-importing total energy counters from EVSE.
313
+ :raises UnsupportedFeature: If gateway firmware is older than 4.0.0.
314
+ :raises CommandFailedError: If the reset operation fails.
315
+ """
316
+ self._require_firmware("4.0.0", "reset_energy_meter")
317
+
318
+ url = f"{self.url}emeter"
319
+ data = {"hard": bool(hard), "import": bool(import_from_evse)}
320
+
321
+ _LOGGER.debug(
322
+ "Resetting energy meter: %s (hard=%s, import=%s)",
323
+ url,
324
+ hard,
325
+ import_from_evse,
326
+ )
327
+ response = await self.process_request(url=url, method="delete", data=data)
328
+ response = self._normalize_response(response)
329
+
330
+ msg = response.get("msg") if isinstance(response, Mapping) else None
331
+ if msg not in SUCCESS_ANSWERS:
332
+ _LOGGER.error("Problem resetting energy meter: %s", response)
333
+ raise CommandFailedError(f"Problem resetting energy meter: {response}")
openevsehttp/const.py CHANGED
@@ -72,6 +72,8 @@ SUCCESS_ANSWERS = [
72
72
  "Deleted",
73
73
  "User name saved",
74
74
  "User name removed",
75
+ "acknowledged",
76
+ "Reset done",
75
77
  ]
76
78
 
77
79
  ERROR_SESSION_REQUIRED = (
@@ -671,3 +671,21 @@ class PropertiesMixin:
671
671
  self._github_token = (
672
672
  token.strip() if isinstance(token, str) and token.strip() else None
673
673
  )
674
+
675
+ @property
676
+ def notifications_count(self) -> int | None:
677
+ """Return unmuted notification advisories count from status summary."""
678
+ notifications = self._status.get("notifications")
679
+ if isinstance(notifications, Mapping):
680
+ return notifications.get("count")
681
+ return None
682
+
683
+ @property
684
+ def notifications_severity(self) -> str | None:
685
+ """Return max severity ('info', 'warning', 'critical') from status summary."""
686
+ notifications = self._status.get("notifications")
687
+ if isinstance(notifications, Mapping):
688
+ severity = notifications.get("severity")
689
+ if severity is not None:
690
+ return str(severity)
691
+ return None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python_openevse_http
3
- Version: 1.7.0
3
+ Version: 1.8.0
4
4
  Summary: Python wrapper for OpenEVSE HTTP API
5
5
  Home-page: https://github.com/firstof9/python-openevse-http
6
6
  Download-URL: https://github.com/firstof9/python-openevse-http
@@ -54,6 +54,11 @@ A Python library for communicating with [OpenEVSE](https://www.openevse.com/) ch
54
54
  - **Time Synchronization & RTC**: Manage time, timezone, and NTP sync (requires firmware 4.0.0+).
55
55
  - **Event Logs**: Retrieve diagnostic log blocks and log event history (requires firmware 4.0.0+).
56
56
  - **Certificates**: Manage SSL/TLS Root CA and client certificates (requires firmware 4.0.0+).
57
+ - **RFID Management**: RFID tag pairing mode and tag-to-user mappings (requires firmware 4.0.0+ / 5.0.0+).
58
+ - **Relay Diagnostics**: Stuck relay recovery cycle and contact-life estimation reset (requires firmware 5.1.0+).
59
+ - **Cable Temperature Monitoring**: Cable temperature sensor configuration and monitoring (requires firmware 5.1.0+).
60
+ - **Energy Meter Reset**: Reset the energy meter counters with soft/hard options (requires firmware 4.0.0+).
61
+ - **Advisory Notifications**: Retrieve gateway advisory notifications and acknowledge/mute active advisories (requires firmware 5.1.0+).
57
62
 
58
63
  ## Installation
59
64
 
@@ -156,7 +161,9 @@ When performing firmware update checks (`charger.firmware_check()`) or automatic
156
161
  | `/time` | GET, POST | ✅ | RTC and NTP time settings (v4.0.0+) |
157
162
  | `/settime` | POST | ✅ | Legacy time setting interface (v3.x / fallback to `$S1` on v2.x) |
158
163
  | `/logs` | GET | ✅ | Event log block bounds and block events (v4.0.0+) |
159
- | `/emeter` | DELETE | ❌ | Energy meter reset |
164
+ | `/emeter` | DELETE | ✅ | Energy meter reset (v4.0.0+) |
165
+ | `/notifications` | GET | ✅ | Gateway advisory notifications list (v5.1.0+) |
166
+ | `/notifications/ack` | POST | ✅ | Acknowledge/mute active advisory notification (v5.1.0+) |
160
167
  | `/wifi` | GET, POST | ❌ | Network scanning and AP configuration |
161
168
  | `/tesla` | GET | ❌ | Tesla vehicle integration |
162
169
  | `/certificates`| GET, POST, DELETE | ✅ | SSL/TLS certificate management (v4.0.0+) |
@@ -1,24 +1,24 @@
1
1
  openevsehttp/__init__.py,sha256=QeXl80ozsK6J9PN-jB_83Bde31YXFGgLuUdmArpZAMM,1363
2
2
  openevsehttp/__main__.py,sha256=EHmSdT7GjAVvHQxvLBTjZXsj_V5SB6B2_kpgUAT7mPM,146
3
- openevsehttp/client.py,sha256=2C2SuRkJaC2r38qo57l1gPV86Fqr8Wp72LBnv2R_tuE,22456
3
+ openevsehttp/client.py,sha256=DRiFOtXEznthZCfD5AGwI6isHMkCd7iiwFgo1W_nux8,22482
4
4
  openevsehttp/commands.py,sha256=l9mxdfgpeYzM1M6NPr0KLIaTtTsU10vzcKG1S7Z6O1M,918
5
5
  openevsehttp/commands_base.py,sha256=DnHnB-rAI7zLDIMkL66epct6KnjLsm9OhlRFlOojREI,1934
6
- openevsehttp/commands_core.py,sha256=c6gzsKqBy1t3mHwlZ6TcMMvQresettVeMvYsGhHQ-Ko,18506
7
- openevsehttp/commands_diagnostics.py,sha256=j23LX0cB9lb2UBkOpmJjNSBQm87AQijlP6qxotAmiTw,9894
6
+ openevsehttp/commands_core.py,sha256=jZZJpD9XolKW0A_1eeec_nB5Kzfbcjih18N7WI_uLvI,18941
7
+ openevsehttp/commands_diagnostics.py,sha256=bPYii1Es4tc-FOFGzb81ODeGIJH_Vmj6093dFe_XVBE,14229
8
8
  openevsehttp/commands_firmware.py,sha256=Ej_EdO8z4arA8wkqfORC0Q_zO9qi7SlC2NmPme03Llg,10308
9
9
  openevsehttp/commands_schedule.py,sha256=kBZp6hPt1qNjoZUVkhRhzB9pXlA--W1UQkQYzxEFQgw,5995
10
10
  openevsehttp/commands_security.py,sha256=--d9weP_Bu-HMhTKphk0qTKgYj1BjM6y2xbD8cccHbI,10911
11
11
  openevsehttp/commands_time.py,sha256=v5Hb0rieHi0SrSj_QQSPU8U3gJlEbRKon81FbH159e8,7737
12
- openevsehttp/const.py,sha256=Dh0hZPSVMXA96QtgxI2BRlmHQuUZWZ1d_U7iHKdmWlM,1801
12
+ openevsehttp/const.py,sha256=jSh-vefm1FE0nQhmgzkWZPOfyjbf2LvcdAlFQYB7vBQ,1839
13
13
  openevsehttp/exceptions.py,sha256=sU6VkuJ2VIDcfbwPdEiwPklYPWb_yR6okRq4zdYhHLY,1654
14
14
  openevsehttp/managers.py,sha256=h_pI0z01FIH_EDVSlg7ZQ3lQVOlOkcb5EIQNQvqf3Mw,5460
15
- openevsehttp/properties.py,sha256=Ak9282qLGXSKzVXoj2PVsGs2bfaHB1YvSBgyQSo7vOQ,22396
15
+ openevsehttp/properties.py,sha256=IsCFSY6gUC4--oYGOFlKfX8_Hzijg1fUXXKJYAscfNw,23117
16
16
  openevsehttp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
17
  openevsehttp/sensors.py,sha256=XrAaL66jkZqh2IjYXpQI5SJc84GduanBmnbCpbta8lA,5797
18
18
  openevsehttp/utils.py,sha256=fPUWL64gFTRw4TtV4WITDAFMJ75VjNhPBaU6diwVlv0,2019
19
19
  openevsehttp/websocket.py,sha256=yMuLNsB2qYZLtfYRW7-D7NH7rv-qaSTp7F79OajKm_0,10790
20
- python_openevse_http-1.7.0.dist-info/licenses/LICENSE,sha256=hSB6TOQ7rmwSGb6XzqRjDGMvmUj5_GlacqQin3tegtA,11341
21
- python_openevse_http-1.7.0.dist-info/METADATA,sha256=K4DJbt66K11Zqi10mFJhE9krqioECU3Wn9ZY94G4rNY,7011
22
- python_openevse_http-1.7.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
23
- python_openevse_http-1.7.0.dist-info/top_level.txt,sha256=u8RUkoEIE33Cjn6gmqiEoVpZ0VZ59WJ3FXBwwOg0CPE,13
24
- python_openevse_http-1.7.0.dist-info/RECORD,,
20
+ python_openevse_http-1.8.0.dist-info/licenses/LICENSE,sha256=hSB6TOQ7rmwSGb6XzqRjDGMvmUj5_GlacqQin3tegtA,11341
21
+ python_openevse_http-1.8.0.dist-info/METADATA,sha256=6XscBROhF-9GvzAci8vBptztH4jdZDMtR9cv-NDv3qo,7803
22
+ python_openevse_http-1.8.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
23
+ python_openevse_http-1.8.0.dist-info/top_level.txt,sha256=u8RUkoEIE33Cjn6gmqiEoVpZ0VZ59WJ3FXBwwOg0CPE,13
24
+ python_openevse_http-1.8.0.dist-info/RECORD,,