python-openevse-http 0.3.4__py3-none-any.whl → 0.4.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/commands.py CHANGED
@@ -12,7 +12,7 @@ from aiohttp.client_exceptions import ContentTypeError, ServerTimeoutError
12
12
  from awesomeversion import AwesomeVersion
13
13
  from awesomeversion.exceptions import AwesomeVersionCompareException
14
14
 
15
- from .const import MAX_AMPS, MIN_AMPS, RAPI_ERRORS, divert_mode
15
+ from .const import MAX_AMPS, MIN_AMPS, RAPI_ERRORS, SUCCESS_ANSWERS, divert_mode
16
16
  from .exceptions import UnknownError, UnsupportedFeature
17
17
  from .utils import get_awesome_version
18
18
 
@@ -68,7 +68,7 @@ class CommandsMixin:
68
68
  response = await self.process_request(url=url, method="post", data=data)
69
69
  response = self._normalize_response(response)
70
70
  msg = response.get("msg") if isinstance(response, Mapping) else None
71
- if msg not in ["OK", "done", "no change"]:
71
+ if msg not in SUCCESS_ANSWERS:
72
72
  _LOGGER.error("Problem issuing command: %s", response)
73
73
  raise UnknownError
74
74
 
@@ -95,11 +95,10 @@ class CommandsMixin:
95
95
  response = await self.process_request(url=url, method="post", data=data)
96
96
  _LOGGER.debug("divert_mode response: %s", response)
97
97
  normalized_response = self._normalize_response(response)
98
- if isinstance(normalized_response, dict) and normalized_response.get("msg") in [
99
- "OK",
100
- "done",
101
- "no change",
102
- ]:
98
+ if (
99
+ isinstance(normalized_response, dict)
100
+ and normalized_response.get("msg") in SUCCESS_ANSWERS
101
+ ):
103
102
  self._config["divert_enabled"] = mode
104
103
  return normalized_response
105
104
 
@@ -172,11 +171,10 @@ class CommandsMixin:
172
171
  response = await self.process_request(url=url, method="patch")
173
172
  response = self._normalize_response(response)
174
173
  _LOGGER.debug("Toggle response: %s", response)
175
- if not isinstance(response, Mapping) or response.get("msg") not in [
176
- "OK",
177
- "done",
178
- "no change",
179
- ]:
174
+ if (
175
+ not isinstance(response, Mapping)
176
+ or response.get("msg") not in SUCCESS_ANSWERS
177
+ ):
180
178
  _LOGGER.error("Problem toggling override: %s", response)
181
179
  raise RuntimeError(f"Failed to toggle override: {response}")
182
180
  else:
@@ -210,7 +208,7 @@ class CommandsMixin:
210
208
  response = self._normalize_response(response)
211
209
  msg = response.get("msg") if isinstance(response, Mapping) else None
212
210
  _LOGGER.debug("Clear override response: %s", msg)
213
- if msg not in ["OK", "done", "no change"]:
211
+ if msg not in SUCCESS_ANSWERS:
214
212
  _LOGGER.error("Problem clearing override: %s", response)
215
213
  raise RuntimeError(f"Failed to clear override: {response}")
216
214
 
@@ -236,11 +234,10 @@ class CommandsMixin:
236
234
  _LOGGER.debug("Setting current limit to %s", amps)
237
235
  response = await self.set_override(charge_current=amps)
238
236
  _LOGGER.debug("Set current response: %s", response)
239
- if not isinstance(response, Mapping) or response.get("msg") not in [
240
- "OK",
241
- "done",
242
- "no change",
243
- ]:
237
+ if (
238
+ not isinstance(response, Mapping)
239
+ or response.get("msg") not in SUCCESS_ANSWERS
240
+ ):
244
241
  _LOGGER.error("Problem setting current limit: %s", response)
245
242
  raise UnknownError
246
243
 
@@ -275,7 +272,7 @@ class CommandsMixin:
275
272
  response = self._normalize_response(response)
276
273
  _LOGGER.debug("service response: %s", response)
277
274
  msg = response.get("msg") if isinstance(response, Mapping) else None
278
- if msg not in ["OK", "done", "no change"]:
275
+ if msg not in SUCCESS_ANSWERS:
279
276
  _LOGGER.error("Problem issuing command: %s", response)
280
277
  raise UnknownError
281
278
 
@@ -417,12 +414,79 @@ class CommandsMixin:
417
414
 
418
415
  if not isinstance(message, dict):
419
416
  return None
417
+
418
+ # Match browser_download_url based on buildenv
419
+ download_url = None
420
+ buildenv = self._config.get("buildenv")
421
+ assets = message.get("assets", [])
422
+
423
+ if buildenv and assets:
424
+ target_filename = f"{buildenv}.bin"
425
+ for asset in assets:
426
+ if asset.get("name") == target_filename:
427
+ download_url = asset.get("browser_download_url")
428
+ break
429
+
420
430
  return {
421
431
  "latest_version": message.get("tag_name"),
422
432
  "release_notes": message.get("body"),
423
433
  "release_url": message.get("html_url"),
434
+ "browser_download_url": download_url,
424
435
  }
425
436
 
437
+ async def update_firmware(
438
+ self,
439
+ firmware_url: str | None = None,
440
+ firmware_bytes: bytes | None = None,
441
+ filename: str = "firmware.bin",
442
+ ) -> Mapping[str, Any] | list[Any] | str:
443
+ """Instruct the device to update its firmware.
444
+
445
+ You can either:
446
+ 1. Pass firmware_bytes to perform a multipart upload of a local file.
447
+ 2. Pass firmware_url to tell the device to download the file directly.
448
+ 3. Pass neither to automatically resolve the latest matching binary URL from GitHub.
449
+ """
450
+ if firmware_bytes is not None and firmware_url is not None:
451
+ raise ValueError("Cannot specify both firmware_bytes and firmware_url")
452
+
453
+ if firmware_url is not None:
454
+ if not isinstance(firmware_url, str) or not firmware_url.strip():
455
+ raise ValueError("Invalid firmware_url")
456
+
457
+ url = f"{self.url}update"
458
+
459
+ # 1. Handle multipart binary upload
460
+ if firmware_bytes is not None:
461
+ form_data = aiohttp.FormData()
462
+ form_data.add_field(
463
+ name="file",
464
+ value=firmware_bytes,
465
+ filename=filename,
466
+ content_type="application/octet-stream",
467
+ )
468
+ _LOGGER.debug(
469
+ "Uploading firmware binary to %s (%d bytes)", url, len(firmware_bytes)
470
+ )
471
+ # Rapi is mapped to http request's data kwarg in process_request
472
+ return await self.process_request(url=url, method="post", rapi=form_data)
473
+
474
+ # 2. Resolve URL from GitHub if not specified
475
+ if firmware_url is None:
476
+ check_result = await self.firmware_check()
477
+ if not check_result or not check_result.get("browser_download_url"):
478
+ raise RuntimeError(
479
+ "Could not resolve latest firmware download URL from GitHub."
480
+ )
481
+ firmware_url = check_result["browser_download_url"]
482
+
483
+ # 3. Post JSON URL payload
484
+ data = {"url": firmware_url}
485
+ _LOGGER.debug(
486
+ "Requesting OpenEVSE to download and update from: %s", firmware_url
487
+ )
488
+ return await self.process_request(url=url, method="post", data=data)
489
+
426
490
  async def set_led_brightness(self, level: int) -> None:
427
491
  """Set LED brightness level."""
428
492
  if isinstance(level, bool) or not isinstance(level, int):
@@ -448,7 +512,7 @@ class CommandsMixin:
448
512
  response = await self.process_request(url=url, method="post", data=data)
449
513
  response = self._normalize_response(response)
450
514
  msg = response.get("msg") if isinstance(response, Mapping) else None
451
- if msg not in ["OK", "done", "no change"]:
515
+ if msg not in SUCCESS_ANSWERS:
452
516
  _LOGGER.error("Problem issuing command: %s", response)
453
517
  raise UnknownError
454
518
 
@@ -470,11 +534,7 @@ class CommandsMixin:
470
534
  res_lower = response.lower()
471
535
  if "divert" in res_lower and "changed" in res_lower:
472
536
  success = True
473
- elif isinstance(response, dict) and response.get("msg") in [
474
- "OK",
475
- "done",
476
- "no change",
477
- ]:
537
+ elif isinstance(response, dict) and response.get("msg") in SUCCESS_ANSWERS:
478
538
  success = True
479
539
 
480
540
  if not success:
@@ -497,7 +557,7 @@ class CommandsMixin:
497
557
  response = await self.process_request(url=url, method="post", rapi=data)
498
558
  response = self._normalize_response(response)
499
559
  msg = response.get("msg") if isinstance(response, Mapping) else None
500
- if msg not in ["OK", "done", "no change", "Current Shaper state changed"]:
560
+ if msg not in SUCCESS_ANSWERS and msg != "Current Shaper state changed":
501
561
  _LOGGER.error("Problem issuing command: %s", response)
502
562
  raise UnknownError
503
563
 
openevsehttp/const.py CHANGED
@@ -60,3 +60,5 @@ RAPI_ERRORS = [
60
60
  "RAPI_RESPONSE_BLOCKED",
61
61
  "RAPI_RESPONSE_INVALID_COMMAND",
62
62
  ]
63
+
64
+ SUCCESS_ANSWERS = ["OK", "done", "no change", "Created", "Updated", "Deleted"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python_openevse_http
3
- Version: 0.3.4
3
+ Version: 0.4.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
@@ -1,16 +1,16 @@
1
1
  openevsehttp/__init__.py,sha256=I6a1mjOZHYiWb_qfCuDuFLOOncrkkB_7uwybtOIujfY,1165
2
2
  openevsehttp/__main__.py,sha256=EHmSdT7GjAVvHQxvLBTjZXsj_V5SB6B2_kpgUAT7mPM,146
3
3
  openevsehttp/client.py,sha256=JBAC1jJGdzOabVAqHv8x6EJDVjhaW4t7Iwqn4lDWhwE,17502
4
- openevsehttp/commands.py,sha256=qro0H2uAzNTIh3LCnP8DgI693J4CKnZkxDRQgWo8HlM,20786
5
- openevsehttp/const.py,sha256=VSc5Xt-KFenft0rT6ql9whCD7J9g_YvjLYU_PX706eE,1360
4
+ openevsehttp/commands.py,sha256=Pv3H0VXWndQica4J4zZlGbxvCz8NjGyJdkppcTnrZcs,23384
5
+ openevsehttp/const.py,sha256=y-2hGv_PCal_-VCSGC7IIyzQYtfeVdq3MjOhBWIdZvc,1440
6
6
  openevsehttp/exceptions.py,sha256=bqz-tHTW1AYJMKcm0s5M6z5tA6XZgjnCiBLW1XrZ_70,672
7
7
  openevsehttp/managers.py,sha256=kEX1ZD9u-FY0UEZJsxeFEGBSGzSlkbBc0kmxCiMJtJw,5373
8
8
  openevsehttp/properties.py,sha256=QVSyn_5a7vI1b4TdnnToRdw6veVCfnp7a19VYit95hg,17107
9
9
  openevsehttp/sensors.py,sha256=sJP2FPnU1Lk5S3VUyFT14JM1nKEBQPsjl-DiZI-pZrs,4673
10
10
  openevsehttp/utils.py,sha256=e3HH_jwZgb1iBWJgIoMOM0JPrQNwXyVdOx5vTWOh4T0,858
11
11
  openevsehttp/websocket.py,sha256=Mi_WFmlT3-9i6bbHIN6ua09SD8CpIle2vRXB3HyWzmM,10066
12
- python_openevse_http-0.3.4.dist-info/licenses/LICENSE,sha256=hSB6TOQ7rmwSGb6XzqRjDGMvmUj5_GlacqQin3tegtA,11341
13
- python_openevse_http-0.3.4.dist-info/METADATA,sha256=xw9ot2rB-084_Du8lm-9Rx58S0mvOpgb7iEbFb4U6sw,4363
14
- python_openevse_http-0.3.4.dist-info/WHEEL,sha256=SmOxYU7pzNKBqASvQJ7DjX3XGUF92lrGhMb3R6_iiqI,91
15
- python_openevse_http-0.3.4.dist-info/top_level.txt,sha256=u8RUkoEIE33Cjn6gmqiEoVpZ0VZ59WJ3FXBwwOg0CPE,13
16
- python_openevse_http-0.3.4.dist-info/RECORD,,
12
+ python_openevse_http-0.4.0.dist-info/licenses/LICENSE,sha256=hSB6TOQ7rmwSGb6XzqRjDGMvmUj5_GlacqQin3tegtA,11341
13
+ python_openevse_http-0.4.0.dist-info/METADATA,sha256=oSaVonGB3ZuiNKcUHN-X-tqW6p3K5Is8wDY8FLjpg7M,4363
14
+ python_openevse_http-0.4.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
15
+ python_openevse_http-0.4.0.dist-info/top_level.txt,sha256=u8RUkoEIE33Cjn6gmqiEoVpZ0VZ59WJ3FXBwwOg0CPE,13
16
+ python_openevse_http-0.4.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (79.0.1)
2
+ Generator: setuptools (82.0.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5