python-openevse-http 0.4.1__tar.gz → 0.4.3__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.
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/.github/workflows/autolabeler.yml +1 -1
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/.github/workflows/links.yml +1 -1
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/.github/workflows/publish-to-pypi.yml +3 -3
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/.github/workflows/release-drafter.yml +1 -1
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/.github/workflows/test.yml +3 -3
- {python_openevse_http-0.4.1/python_openevse_http.egg-info → python_openevse_http-0.4.3}/PKG-INFO +1 -1
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/openevsehttp/client.py +3 -3
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/openevsehttp/commands.py +90 -5
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/pyproject.toml +1 -1
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3/python_openevse_http.egg-info}/PKG-INFO +1 -1
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/requirements_test.txt +0 -1
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/tests/conftest.py +176 -3
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/tests/test_client.py +49 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/tests/test_commands.py +168 -20
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/tests/test_managers.py +58 -15
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/.github/dependabot.yml +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/.github/release-drafter.yml +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/.gitignore +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/.pre-commit-config.yaml +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/.yamllint +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/EXTERNAL_SESSION.md +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/LICENSE +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/README.md +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/codecov.yml +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/example_external_session.py +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/openevsehttp/__init__.py +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/openevsehttp/__main__.py +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/openevsehttp/const.py +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/openevsehttp/exceptions.py +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/openevsehttp/managers.py +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/openevsehttp/properties.py +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/openevsehttp/sensors.py +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/openevsehttp/utils.py +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/openevsehttp/websocket.py +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/python_openevse_http.egg-info/SOURCES.txt +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/python_openevse_http.egg-info/dependency_links.txt +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/python_openevse_http.egg-info/not-zip-safe +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/python_openevse_http.egg-info/requires.txt +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/python_openevse_http.egg-info/top_level.txt +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/requirements.txt +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/requirements_lint.txt +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/setup.cfg +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/setup.py +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/tests/__init__.py +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/tests/common.py +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/tests/fixtures/github_v2.json +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/tests/fixtures/github_v4.json +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/tests/fixtures/v2_json/config.json +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/tests/fixtures/v2_json/status.json +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/tests/fixtures/v4_json/config-broken-semver.json +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/tests/fixtures/v4_json/config-broken.json +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/tests/fixtures/v4_json/config-dev.json +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/tests/fixtures/v4_json/config-extra-version.json +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/tests/fixtures/v4_json/config-new.json +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/tests/fixtures/v4_json/config-unknown-semver.json +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/tests/fixtures/v4_json/config.json +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/tests/fixtures/v4_json/schedule.json +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/tests/fixtures/v4_json/status-broken.json +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/tests/fixtures/v4_json/status-new.json +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/tests/fixtures/v4_json/status.json +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/tests/fixtures/websocket.json +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/tests/test_external_session.py +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/tests/test_main_edge_cases.py +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/tests/test_mixins.py +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/tests/test_properties.py +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/tests/test_sensors.py +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/tests/test_shaper.py +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/tests/test_websocket.py +0 -0
- {python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/tox.ini +0 -0
|
@@ -32,7 +32,7 @@ jobs:
|
|
|
32
32
|
runs-on: ubuntu-latest
|
|
33
33
|
timeout-minutes: 3
|
|
34
34
|
steps:
|
|
35
|
-
- uses: step-security/harden-runner@
|
|
35
|
+
- uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
|
|
36
36
|
with:
|
|
37
37
|
egress-policy: audit
|
|
38
38
|
|
|
@@ -15,7 +15,7 @@ jobs:
|
|
|
15
15
|
linkChecker:
|
|
16
16
|
runs-on: ubuntu-latest
|
|
17
17
|
steps:
|
|
18
|
-
- uses: step-security/harden-runner@
|
|
18
|
+
- uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
|
|
19
19
|
with:
|
|
20
20
|
egress-policy: audit
|
|
21
21
|
|
{python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/.github/workflows/publish-to-pypi.yml
RENAMED
|
@@ -21,7 +21,7 @@ jobs:
|
|
|
21
21
|
contents: read
|
|
22
22
|
id-token: write
|
|
23
23
|
steps:
|
|
24
|
-
- uses: step-security/harden-runner@
|
|
24
|
+
- uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
|
|
25
25
|
with:
|
|
26
26
|
egress-policy: audit
|
|
27
27
|
|
|
@@ -31,7 +31,7 @@ jobs:
|
|
|
31
31
|
fetch-depth: 0
|
|
32
32
|
|
|
33
33
|
- name: Install uv
|
|
34
|
-
uses: astral-sh/setup-uv@
|
|
34
|
+
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
|
35
35
|
with:
|
|
36
36
|
enable-cache: true
|
|
37
37
|
version: "0.10.9"
|
|
@@ -43,4 +43,4 @@ jobs:
|
|
|
43
43
|
run: uv build
|
|
44
44
|
|
|
45
45
|
- name: Publish release to PyPI
|
|
46
|
-
uses: pypa/gh-action-pypi-publish@
|
|
46
|
+
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
|
{python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/.github/workflows/release-drafter.yml
RENAMED
|
@@ -13,7 +13,7 @@ jobs:
|
|
|
13
13
|
update_release_draft:
|
|
14
14
|
runs-on: ubuntu-latest
|
|
15
15
|
steps:
|
|
16
|
-
- uses: step-security/harden-runner@
|
|
16
|
+
- uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
|
|
17
17
|
with:
|
|
18
18
|
egress-policy: audit
|
|
19
19
|
|
|
@@ -18,7 +18,7 @@ jobs:
|
|
|
18
18
|
prek:
|
|
19
19
|
runs-on: ubuntu-latest
|
|
20
20
|
steps:
|
|
21
|
-
- uses: step-security/harden-runner@
|
|
21
|
+
- uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
|
|
22
22
|
with:
|
|
23
23
|
egress-policy: audit
|
|
24
24
|
|
|
@@ -41,7 +41,7 @@ jobs:
|
|
|
41
41
|
- "3.14"
|
|
42
42
|
|
|
43
43
|
steps:
|
|
44
|
-
- uses: step-security/harden-runner@
|
|
44
|
+
- uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
|
|
45
45
|
with:
|
|
46
46
|
egress-policy: audit
|
|
47
47
|
|
|
@@ -69,7 +69,7 @@ jobs:
|
|
|
69
69
|
runs-on: ubuntu-latest
|
|
70
70
|
needs: build
|
|
71
71
|
steps:
|
|
72
|
-
- uses: step-security/harden-runner@
|
|
72
|
+
- uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
|
|
73
73
|
with:
|
|
74
74
|
egress-policy: audit
|
|
75
75
|
|
|
@@ -183,12 +183,12 @@ class OpenEVSE(CommandsMixin, ManagersMixin, SensorsMixin, PropertiesMixin):
|
|
|
183
183
|
return (False, "")
|
|
184
184
|
return (value["cmd"], value["ret"])
|
|
185
185
|
|
|
186
|
-
async def update(self) -> None:
|
|
186
|
+
async def update(self, force_status: bool = False) -> None:
|
|
187
187
|
"""Update the values."""
|
|
188
188
|
# TODO: add addiontal endpoints to update
|
|
189
189
|
urls = [f"{self.url}config"]
|
|
190
190
|
|
|
191
|
-
if not self._ws_listening:
|
|
191
|
+
if not self._ws_listening or force_status or self.ota_update:
|
|
192
192
|
urls = [f"{self.url}status", f"{self.url}config"]
|
|
193
193
|
|
|
194
194
|
for url in urls:
|
|
@@ -196,7 +196,7 @@ class OpenEVSE(CommandsMixin, ManagersMixin, SensorsMixin, PropertiesMixin):
|
|
|
196
196
|
response = await self.process_request(url, method="get")
|
|
197
197
|
if "/status" in url:
|
|
198
198
|
if isinstance(response, Mapping) and "error" not in response:
|
|
199
|
-
self._status
|
|
199
|
+
self._status.update(dict(response))
|
|
200
200
|
_LOGGER.debug("Status update: %s", self._status)
|
|
201
201
|
elif isinstance(response, Mapping):
|
|
202
202
|
_LOGGER.warning(
|
|
@@ -39,13 +39,27 @@ class CommandsMixin:
|
|
|
39
39
|
async def send_command(self, command: str) -> tuple:
|
|
40
40
|
raise NotImplementedError
|
|
41
41
|
|
|
42
|
-
async def update(self) -> None:
|
|
42
|
+
async def update(self, force_status: bool = False) -> None:
|
|
43
43
|
raise NotImplementedError
|
|
44
44
|
|
|
45
45
|
def _normalize_response(self, response: Any) -> dict[str, Any] | list[Any]:
|
|
46
46
|
"""Normalize response to a dict or list."""
|
|
47
47
|
raise NotImplementedError
|
|
48
48
|
|
|
49
|
+
def _flag_ota_if_started(self, response: Any) -> None:
|
|
50
|
+
"""Flag OTA as active if response indicates firmware update has started."""
|
|
51
|
+
normalized = self._normalize_response(response)
|
|
52
|
+
if isinstance(normalized, dict) and (
|
|
53
|
+
normalized.get("msg") == "started"
|
|
54
|
+
or normalized.get("msg") in SUCCESS_ANSWERS
|
|
55
|
+
):
|
|
56
|
+
_LOGGER.debug("Firmware update started, setting ota_update flag.")
|
|
57
|
+
self._status["ota_update"] = 1
|
|
58
|
+
else:
|
|
59
|
+
_LOGGER.debug(
|
|
60
|
+
"Firmware update response did not indicate start: %s", normalized
|
|
61
|
+
)
|
|
62
|
+
|
|
49
63
|
async def get_schedule(self) -> Mapping[str, Any] | list[Any]:
|
|
50
64
|
"""Return the current schedule."""
|
|
51
65
|
url = f"{self.url}schedule"
|
|
@@ -122,7 +136,12 @@ class CommandsMixin:
|
|
|
122
136
|
time_limit: int | None = None,
|
|
123
137
|
auto_release: bool | None = None,
|
|
124
138
|
) -> Any:
|
|
125
|
-
"""Set the manual override status.
|
|
139
|
+
"""Set the manual override status.
|
|
140
|
+
|
|
141
|
+
Fetches the current override payload first and merges existing values
|
|
142
|
+
into the request payload. This prevents the firmware from clearing/resetting
|
|
143
|
+
previously configured properties that are not passed in the function call.
|
|
144
|
+
"""
|
|
126
145
|
if not self._version_check("4.0.1"):
|
|
127
146
|
_LOGGER.debug("Feature not supported for older firmware.")
|
|
128
147
|
raise UnsupportedFeature
|
|
@@ -140,6 +159,18 @@ class CommandsMixin:
|
|
|
140
159
|
raise ValueError
|
|
141
160
|
|
|
142
161
|
data: dict[str, Any] = {}
|
|
162
|
+
if isinstance(response, Mapping):
|
|
163
|
+
for key in (
|
|
164
|
+
"state",
|
|
165
|
+
"charge_current",
|
|
166
|
+
"max_current",
|
|
167
|
+
"energy_limit",
|
|
168
|
+
"time_limit",
|
|
169
|
+
"auto_release",
|
|
170
|
+
):
|
|
171
|
+
if key in response:
|
|
172
|
+
data[key] = response[key]
|
|
173
|
+
|
|
143
174
|
if auto_release is not None:
|
|
144
175
|
data["auto_release"] = auto_release
|
|
145
176
|
|
|
@@ -372,6 +403,7 @@ class CommandsMixin:
|
|
|
372
403
|
url = f"{base_url}ESP32_WiFi_V4.x/releases/latest"
|
|
373
404
|
else:
|
|
374
405
|
url = f"{base_url}ESP8266_WiFi_v2.x/releases/latest"
|
|
406
|
+
_LOGGER.debug("Firmware check URL: %s", url)
|
|
375
407
|
except AwesomeVersionCompareException:
|
|
376
408
|
_LOGGER.debug("Non-semver firmware version detected.")
|
|
377
409
|
return None
|
|
@@ -403,6 +435,7 @@ class CommandsMixin:
|
|
|
403
435
|
method,
|
|
404
436
|
)
|
|
405
437
|
async with http_method(url) as resp:
|
|
438
|
+
_LOGGER.debug("Firmware check response status: %d", resp.status)
|
|
406
439
|
if resp.status != 200:
|
|
407
440
|
return None
|
|
408
441
|
message = await resp.text()
|
|
@@ -413,19 +446,51 @@ class CommandsMixin:
|
|
|
413
446
|
return None
|
|
414
447
|
|
|
415
448
|
if not isinstance(message, dict):
|
|
449
|
+
_LOGGER.debug(
|
|
450
|
+
"Invalid JSON response type from GitHub: %s", type(message)
|
|
451
|
+
)
|
|
416
452
|
return None
|
|
417
453
|
|
|
454
|
+
_LOGGER.debug(
|
|
455
|
+
"GitHub release metadata successfully fetched for version: %s",
|
|
456
|
+
message.get("tag_name"),
|
|
457
|
+
)
|
|
458
|
+
|
|
418
459
|
# Match browser_download_url based on buildenv
|
|
419
460
|
download_url = None
|
|
420
461
|
buildenv = self._config.get("buildenv")
|
|
421
462
|
assets = message.get("assets", [])
|
|
422
463
|
|
|
423
|
-
if buildenv
|
|
464
|
+
if not buildenv:
|
|
465
|
+
_LOGGER.debug(
|
|
466
|
+
"Cannot resolve firmware asset: missing buildenv in config."
|
|
467
|
+
)
|
|
468
|
+
assets = []
|
|
469
|
+
elif not isinstance(assets, list):
|
|
470
|
+
_LOGGER.debug("Invalid GitHub assets payload: %r", assets)
|
|
471
|
+
assets = []
|
|
472
|
+
else:
|
|
473
|
+
_LOGGER.debug("Matching buildenv '%s' against assets", buildenv)
|
|
424
474
|
target_filename = f"{buildenv}.bin"
|
|
425
475
|
for asset in assets:
|
|
476
|
+
if not isinstance(asset, Mapping):
|
|
477
|
+
continue
|
|
426
478
|
if asset.get("name") == target_filename:
|
|
427
479
|
download_url = asset.get("browser_download_url")
|
|
480
|
+
_LOGGER.debug("Found matching firmware asset: %s", download_url)
|
|
428
481
|
break
|
|
482
|
+
if buildenv and not download_url:
|
|
483
|
+
_LOGGER.debug(
|
|
484
|
+
"Could not find asset matching target filename '%s.bin' in assets: %s",
|
|
485
|
+
buildenv,
|
|
486
|
+
[
|
|
487
|
+
asset.get("name")
|
|
488
|
+
for asset in assets
|
|
489
|
+
if isinstance(asset, Mapping)
|
|
490
|
+
]
|
|
491
|
+
if assets
|
|
492
|
+
else "None",
|
|
493
|
+
)
|
|
429
494
|
|
|
430
495
|
return {
|
|
431
496
|
"latest_version": message.get("tag_name"),
|
|
@@ -452,10 +517,16 @@ class CommandsMixin:
|
|
|
452
517
|
raise UnsupportedFeature
|
|
453
518
|
|
|
454
519
|
if firmware_bytes is not None and firmware_url is not None:
|
|
520
|
+
_LOGGER.error("Cannot specify both firmware_bytes and firmware_url")
|
|
455
521
|
raise ValueError("Cannot specify both firmware_bytes and firmware_url")
|
|
456
522
|
|
|
523
|
+
if firmware_bytes is not None and len(firmware_bytes) == 0:
|
|
524
|
+
_LOGGER.error("Empty firmware bytes provided")
|
|
525
|
+
raise ValueError("Empty firmware bytes provided")
|
|
526
|
+
|
|
457
527
|
if firmware_url is not None:
|
|
458
528
|
if not isinstance(firmware_url, str) or not firmware_url.strip():
|
|
529
|
+
_LOGGER.error("Invalid firmware_url: %s", firmware_url)
|
|
459
530
|
raise ValueError("Invalid firmware_url")
|
|
460
531
|
|
|
461
532
|
url = f"{self.url}update"
|
|
@@ -473,12 +544,23 @@ class CommandsMixin:
|
|
|
473
544
|
"Uploading firmware binary to %s (%d bytes)", url, len(firmware_bytes)
|
|
474
545
|
)
|
|
475
546
|
# Rapi is mapped to http request's data kwarg in process_request
|
|
476
|
-
|
|
547
|
+
response = await self.process_request(
|
|
548
|
+
url=url, method="post", rapi=form_data
|
|
549
|
+
)
|
|
550
|
+
_LOGGER.debug("Firmware upload request completed. Response: %s", response)
|
|
551
|
+
self._flag_ota_if_started(response)
|
|
552
|
+
return response
|
|
477
553
|
|
|
478
554
|
# 2. Resolve URL from GitHub if not specified
|
|
479
555
|
if firmware_url is None:
|
|
556
|
+
_LOGGER.debug(
|
|
557
|
+
"No firmware URL provided. Resolving latest matching firmware from GitHub."
|
|
558
|
+
)
|
|
480
559
|
check_result = await self.firmware_check()
|
|
481
560
|
if not check_result or not check_result.get("browser_download_url"):
|
|
561
|
+
_LOGGER.error(
|
|
562
|
+
"Could not resolve latest firmware download URL from GitHub."
|
|
563
|
+
)
|
|
482
564
|
raise RuntimeError(
|
|
483
565
|
"Could not resolve latest firmware download URL from GitHub."
|
|
484
566
|
)
|
|
@@ -489,7 +571,10 @@ class CommandsMixin:
|
|
|
489
571
|
_LOGGER.debug(
|
|
490
572
|
"Requesting OpenEVSE to download and update from: %s", firmware_url
|
|
491
573
|
)
|
|
492
|
-
|
|
574
|
+
response = await self.process_request(url=url, method="post", data=data)
|
|
575
|
+
_LOGGER.debug("Firmware update request completed. Response: %s", response)
|
|
576
|
+
self._flag_ota_if_started(response)
|
|
577
|
+
return response
|
|
493
578
|
|
|
494
579
|
async def set_led_brightness(self, level: int) -> None:
|
|
495
580
|
"""Set LED brightness level."""
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
"""Provide common pytest fixtures."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
from typing import Any, NamedTuple
|
|
5
|
+
from unittest.mock import patch
|
|
4
6
|
|
|
7
|
+
import aiohttp
|
|
5
8
|
import pytest
|
|
6
|
-
from
|
|
9
|
+
from multidict import CIMultiDict
|
|
7
10
|
|
|
8
11
|
import openevsehttp as main
|
|
9
12
|
from tests.common import load_fixture
|
|
@@ -226,10 +229,180 @@ def test_charger_v4_0(mock_aioclient):
|
|
|
226
229
|
return main.OpenEVSE(TEST_TLD)
|
|
227
230
|
|
|
228
231
|
|
|
232
|
+
class MockResponse:
|
|
233
|
+
def __init__(
|
|
234
|
+
self,
|
|
235
|
+
method: str,
|
|
236
|
+
url: str,
|
|
237
|
+
status: int,
|
|
238
|
+
body: Any,
|
|
239
|
+
headers: dict | None,
|
|
240
|
+
content_type: str,
|
|
241
|
+
):
|
|
242
|
+
self.method = method
|
|
243
|
+
self.url = url
|
|
244
|
+
self.status = status
|
|
245
|
+
self._body = body
|
|
246
|
+
self.headers = CIMultiDict(headers or {})
|
|
247
|
+
if content_type:
|
|
248
|
+
self.headers.setdefault("content-type", content_type)
|
|
249
|
+
|
|
250
|
+
async def text(self, encoding: str | None = None, errors: str = "strict") -> str:
|
|
251
|
+
if isinstance(self._body, bytes):
|
|
252
|
+
return self._body.decode(encoding or "utf-8", errors=errors)
|
|
253
|
+
if isinstance(self._body, str):
|
|
254
|
+
return self._body
|
|
255
|
+
|
|
256
|
+
return json.dumps(self._body)
|
|
257
|
+
|
|
258
|
+
async def read(self) -> bytes:
|
|
259
|
+
if isinstance(self._body, bytes):
|
|
260
|
+
return self._body
|
|
261
|
+
if isinstance(self._body, str):
|
|
262
|
+
return self._body.encode("utf-8")
|
|
263
|
+
|
|
264
|
+
return json.dumps(self._body).encode("utf-8")
|
|
265
|
+
|
|
266
|
+
async def json(self, *args, **kwargs) -> Any:
|
|
267
|
+
if isinstance(self._body, bytes):
|
|
268
|
+
return json.loads(self._body.decode("utf-8"))
|
|
269
|
+
if isinstance(self._body, str):
|
|
270
|
+
return json.loads(self._body)
|
|
271
|
+
return self._body
|
|
272
|
+
|
|
273
|
+
async def __aenter__(self):
|
|
274
|
+
return self
|
|
275
|
+
|
|
276
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
277
|
+
pass
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
class RegisteredMock(NamedTuple):
|
|
281
|
+
method: str
|
|
282
|
+
url_pattern: Any
|
|
283
|
+
status: int
|
|
284
|
+
body: Any
|
|
285
|
+
exception: Any
|
|
286
|
+
repeat: bool
|
|
287
|
+
content_type: str
|
|
288
|
+
headers: dict | None
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class AiohttpClientMocker:
|
|
292
|
+
def __init__(self):
|
|
293
|
+
self.mocks = []
|
|
294
|
+
self.requests = []
|
|
295
|
+
self._patcher = None
|
|
296
|
+
|
|
297
|
+
def __enter__(self):
|
|
298
|
+
self.start()
|
|
299
|
+
return self
|
|
300
|
+
|
|
301
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
302
|
+
self.stop()
|
|
303
|
+
|
|
304
|
+
def start(self):
|
|
305
|
+
self._patcher = patch.object(
|
|
306
|
+
aiohttp.ClientSession, "_request", new=self._request_mock
|
|
307
|
+
)
|
|
308
|
+
self._patcher.start()
|
|
309
|
+
|
|
310
|
+
def stop(self):
|
|
311
|
+
if self._patcher:
|
|
312
|
+
self._patcher.stop()
|
|
313
|
+
|
|
314
|
+
def add(
|
|
315
|
+
self,
|
|
316
|
+
method: str,
|
|
317
|
+
url: Any,
|
|
318
|
+
status: int = 200,
|
|
319
|
+
body: Any = "",
|
|
320
|
+
exception: Any = None,
|
|
321
|
+
repeat: bool = False,
|
|
322
|
+
content_type: str = "application/json",
|
|
323
|
+
headers: dict | None = None,
|
|
324
|
+
):
|
|
325
|
+
self.mocks.append(
|
|
326
|
+
RegisteredMock(
|
|
327
|
+
method=method.upper(),
|
|
328
|
+
url_pattern=url,
|
|
329
|
+
status=status,
|
|
330
|
+
body=body,
|
|
331
|
+
exception=exception,
|
|
332
|
+
repeat=repeat,
|
|
333
|
+
content_type=content_type,
|
|
334
|
+
headers=headers,
|
|
335
|
+
)
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
def get(self, *args, **kwargs):
|
|
339
|
+
self.add("GET", *args, **kwargs)
|
|
340
|
+
|
|
341
|
+
def post(self, *args, **kwargs):
|
|
342
|
+
self.add("POST", *args, **kwargs)
|
|
343
|
+
|
|
344
|
+
def put(self, *args, **kwargs):
|
|
345
|
+
self.add("PUT", *args, **kwargs)
|
|
346
|
+
|
|
347
|
+
def delete(self, *args, **kwargs):
|
|
348
|
+
self.add("DELETE", *args, **kwargs)
|
|
349
|
+
|
|
350
|
+
def patch(self, *args, **kwargs):
|
|
351
|
+
self.add("PATCH", *args, **kwargs)
|
|
352
|
+
|
|
353
|
+
def head(self, *args, **kwargs):
|
|
354
|
+
self.add("HEAD", *args, **kwargs)
|
|
355
|
+
|
|
356
|
+
def options(self, *args, **kwargs):
|
|
357
|
+
self.add("OPTIONS", *args, **kwargs)
|
|
358
|
+
|
|
359
|
+
async def _request_mock(self, method: str, str_or_url: Any, **kwargs: Any):
|
|
360
|
+
url_str = str(str_or_url)
|
|
361
|
+
method_upper = method.upper()
|
|
362
|
+
self.requests.append((method_upper, url_str, kwargs))
|
|
363
|
+
|
|
364
|
+
matching_mock = None
|
|
365
|
+
matching_index = -1
|
|
366
|
+
|
|
367
|
+
for i, mock in enumerate(self.mocks):
|
|
368
|
+
if mock.method != method_upper:
|
|
369
|
+
continue
|
|
370
|
+
matched = False
|
|
371
|
+
if hasattr(mock.url_pattern, "match"):
|
|
372
|
+
if mock.url_pattern.match(url_str):
|
|
373
|
+
matched = True
|
|
374
|
+
elif isinstance(mock.url_pattern, str):
|
|
375
|
+
if mock.url_pattern == url_str:
|
|
376
|
+
matched = True
|
|
377
|
+
|
|
378
|
+
if matched:
|
|
379
|
+
matching_mock = mock
|
|
380
|
+
matching_index = i
|
|
381
|
+
break
|
|
382
|
+
|
|
383
|
+
if not matching_mock:
|
|
384
|
+
raise AssertionError(f"No mock registered for {method_upper} {url_str}")
|
|
385
|
+
|
|
386
|
+
if not matching_mock.repeat:
|
|
387
|
+
self.mocks.pop(matching_index)
|
|
388
|
+
|
|
389
|
+
if matching_mock.exception:
|
|
390
|
+
raise matching_mock.exception
|
|
391
|
+
|
|
392
|
+
return MockResponse(
|
|
393
|
+
method=method_upper,
|
|
394
|
+
url=url_str,
|
|
395
|
+
status=matching_mock.status,
|
|
396
|
+
body=matching_mock.body,
|
|
397
|
+
headers=matching_mock.headers,
|
|
398
|
+
content_type=matching_mock.content_type,
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
|
|
229
402
|
@pytest.fixture
|
|
230
403
|
def aioclient_mock():
|
|
231
404
|
"""Fixture to mock aioclient calls."""
|
|
232
|
-
with
|
|
405
|
+
with AiohttpClientMocker() as mock_aiohttp:
|
|
233
406
|
mock_headers = {"content-type": "application/json"}
|
|
234
407
|
mock_aiohttp.get(
|
|
235
408
|
"ws://openevse.test.tld/ws",
|
|
@@ -244,5 +417,5 @@ def aioclient_mock():
|
|
|
244
417
|
@pytest.fixture
|
|
245
418
|
def mock_aioclient():
|
|
246
419
|
"""Fixture to mock aioclient calls."""
|
|
247
|
-
with
|
|
420
|
+
with AiohttpClientMocker() as m:
|
|
248
421
|
yield m
|
|
@@ -117,6 +117,55 @@ async def test_get_status_auth_err(test_charger_auth_err):
|
|
|
117
117
|
await test_charger_auth_err.update()
|
|
118
118
|
|
|
119
119
|
|
|
120
|
+
async def test_update_force_status(mock_aioclient):
|
|
121
|
+
"""Test force_status parameter when ws is listening."""
|
|
122
|
+
unique_host = "force-status.test.tld"
|
|
123
|
+
charger = OpenEVSE(unique_host)
|
|
124
|
+
url_status = f"http://{unique_host}/status"
|
|
125
|
+
url_config = f"http://{unique_host}/config"
|
|
126
|
+
|
|
127
|
+
mock_aioclient.get(
|
|
128
|
+
url_status,
|
|
129
|
+
status=200,
|
|
130
|
+
body='{"state": 2, "wifi_serial": "123"}',
|
|
131
|
+
)
|
|
132
|
+
mock_aioclient.get(
|
|
133
|
+
url_config,
|
|
134
|
+
status=200,
|
|
135
|
+
body='{"wifi_serial": "123", "version": "4.0.1"}',
|
|
136
|
+
)
|
|
137
|
+
charger._ws_listening = True
|
|
138
|
+
charger._status = {"transient_key": "preserved"}
|
|
139
|
+
await charger.update(force_status=True)
|
|
140
|
+
assert charger._status["state"] == 2
|
|
141
|
+
assert charger._status["transient_key"] == "preserved"
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
async def test_update_ota_active(mock_aioclient):
|
|
145
|
+
"""Test automatic status polling when ota_update is active."""
|
|
146
|
+
unique_host = "ota-active.test.tld"
|
|
147
|
+
charger = OpenEVSE(unique_host)
|
|
148
|
+
url_status = f"http://{unique_host}/status"
|
|
149
|
+
url_config = f"http://{unique_host}/config"
|
|
150
|
+
|
|
151
|
+
mock_aioclient.get(
|
|
152
|
+
url_status,
|
|
153
|
+
status=200,
|
|
154
|
+
body='{"state": 2, "ota_update": 1}',
|
|
155
|
+
)
|
|
156
|
+
mock_aioclient.get(
|
|
157
|
+
url_config,
|
|
158
|
+
status=200,
|
|
159
|
+
body='{"wifi_serial": "123", "version": "4.0.1"}',
|
|
160
|
+
)
|
|
161
|
+
charger._ws_listening = True
|
|
162
|
+
charger._status = {"ota_update": 1, "ota_progress": 50}
|
|
163
|
+
await charger.update()
|
|
164
|
+
assert charger._status["state"] == 2
|
|
165
|
+
assert charger._status["ota_progress"] == 50
|
|
166
|
+
assert charger.ota_update is True
|
|
167
|
+
|
|
168
|
+
|
|
120
169
|
# ── send_command ──────────────────────────────────────────────────────
|
|
121
170
|
|
|
122
171
|
|
|
@@ -1060,6 +1060,9 @@ async def test_update_firmware_bytes(test_charger, mock_aioclient, caplog):
|
|
|
1060
1060
|
"Uploading firmware binary to http://openevse.test.tld/update (14 bytes)"
|
|
1061
1061
|
in caplog.text
|
|
1062
1062
|
)
|
|
1063
|
+
assert "Firmware upload request completed. Response: OK" in caplog.text
|
|
1064
|
+
assert "Firmware update started, setting ota_update flag." in caplog.text
|
|
1065
|
+
assert test_charger.ota_update is True
|
|
1063
1066
|
|
|
1064
1067
|
|
|
1065
1068
|
async def test_update_firmware_url(test_charger, mock_aioclient, caplog):
|
|
@@ -1079,6 +1082,12 @@ async def test_update_firmware_url(test_charger, mock_aioclient, caplog):
|
|
|
1079
1082
|
"Requesting OpenEVSE to download and update from: http://github.com/release.bin"
|
|
1080
1083
|
in caplog.text
|
|
1081
1084
|
)
|
|
1085
|
+
assert (
|
|
1086
|
+
"Firmware update request completed. Response: {'msg': 'started'}"
|
|
1087
|
+
in caplog.text
|
|
1088
|
+
)
|
|
1089
|
+
assert "Firmware update started, setting ota_update flag." in caplog.text
|
|
1090
|
+
assert test_charger.ota_update is True
|
|
1082
1091
|
|
|
1083
1092
|
|
|
1084
1093
|
async def test_update_firmware_auto(test_charger, mock_aioclient, caplog):
|
|
@@ -1118,13 +1127,43 @@ async def test_update_firmware_auto(test_charger, mock_aioclient, caplog):
|
|
|
1118
1127
|
with caplog.at_level(logging.DEBUG):
|
|
1119
1128
|
response = await test_charger.update_firmware()
|
|
1120
1129
|
assert response == {"msg": "started"}
|
|
1130
|
+
assert (
|
|
1131
|
+
"No firmware URL provided. Resolving latest matching firmware from GitHub."
|
|
1132
|
+
in caplog.text
|
|
1133
|
+
)
|
|
1134
|
+
assert "Detected firmware: 4.1.7" in caplog.text
|
|
1135
|
+
assert "Using version: 4.1.7" in caplog.text
|
|
1136
|
+
assert (
|
|
1137
|
+
"Firmware check URL: https://api.github.com/repos/OpenEVSE/ESP32_WiFi_V4.x/releases/latest"
|
|
1138
|
+
in caplog.text
|
|
1139
|
+
)
|
|
1140
|
+
assert "Firmware check response status: 200" in caplog.text
|
|
1141
|
+
assert (
|
|
1142
|
+
"GitHub release metadata successfully fetched for version: v4.1.2"
|
|
1143
|
+
in caplog.text
|
|
1144
|
+
)
|
|
1145
|
+
assert (
|
|
1146
|
+
"Matching buildenv 'openevse_esp32-gateway' against assets" in caplog.text
|
|
1147
|
+
)
|
|
1148
|
+
assert (
|
|
1149
|
+
"Found matching firmware asset: https://github.com/OpenEVSE/releases/download/v4.1.2/openevse_esp32-gateway.bin"
|
|
1150
|
+
in caplog.text
|
|
1151
|
+
)
|
|
1121
1152
|
assert (
|
|
1122
1153
|
"Requesting OpenEVSE to download and update from: https://github.com/OpenEVSE/releases/download/v4.1.2/openevse_esp32-gateway.bin"
|
|
1123
1154
|
in caplog.text
|
|
1124
1155
|
)
|
|
1156
|
+
assert (
|
|
1157
|
+
"Firmware update request completed. Response: {'msg': 'started'}"
|
|
1158
|
+
in caplog.text
|
|
1159
|
+
)
|
|
1160
|
+
assert "Firmware update started, setting ota_update flag." in caplog.text
|
|
1161
|
+
assert test_charger.ota_update is True
|
|
1125
1162
|
|
|
1126
1163
|
|
|
1127
|
-
async def test_update_firmware_auto_missing_buildenv(
|
|
1164
|
+
async def test_update_firmware_auto_missing_buildenv(
|
|
1165
|
+
test_charger, mock_aioclient, caplog
|
|
1166
|
+
):
|
|
1128
1167
|
"""Test update_firmware raises RuntimeError when buildenv asset is missing."""
|
|
1129
1168
|
test_charger._config = {"version": "4.1.7", "buildenv": "openevse_esp32-gateway"}
|
|
1130
1169
|
|
|
@@ -1147,35 +1186,47 @@ async def test_update_firmware_auto_missing_buildenv(test_charger, mock_aioclien
|
|
|
1147
1186
|
body=json.dumps(github_response),
|
|
1148
1187
|
)
|
|
1149
1188
|
|
|
1150
|
-
with
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1189
|
+
with caplog.at_level(logging.DEBUG):
|
|
1190
|
+
with pytest.raises(
|
|
1191
|
+
RuntimeError,
|
|
1192
|
+
match=r"Could not resolve latest firmware download URL from GitHub\.",
|
|
1193
|
+
):
|
|
1194
|
+
await test_charger.update_firmware()
|
|
1195
|
+
assert (
|
|
1196
|
+
"Could not find asset matching target filename 'openevse_esp32-gateway.bin' in assets: ['other_env.bin']"
|
|
1197
|
+
in caplog.text
|
|
1198
|
+
)
|
|
1199
|
+
assert (
|
|
1200
|
+
"Could not resolve latest firmware download URL from GitHub." in caplog.text
|
|
1201
|
+
)
|
|
1155
1202
|
|
|
1156
1203
|
|
|
1157
|
-
async def test_update_firmware_both_provided(test_charger):
|
|
1204
|
+
async def test_update_firmware_both_provided(test_charger, caplog):
|
|
1158
1205
|
"""Test update_firmware raises ValueError when both bytes and URL are provided."""
|
|
1159
1206
|
test_charger._config["version"] = "4.1.7"
|
|
1160
|
-
with
|
|
1161
|
-
ValueError
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1207
|
+
with caplog.at_level(logging.DEBUG):
|
|
1208
|
+
with pytest.raises(ValueError):
|
|
1209
|
+
await test_charger.update_firmware(
|
|
1210
|
+
firmware_url="http://url", firmware_bytes=b"bytes"
|
|
1211
|
+
)
|
|
1212
|
+
assert "Cannot specify both firmware_bytes and firmware_url" in caplog.text
|
|
1166
1213
|
|
|
1167
1214
|
|
|
1168
|
-
async def test_update_firmware_url_invalid(test_charger):
|
|
1215
|
+
async def test_update_firmware_url_invalid(test_charger, caplog):
|
|
1169
1216
|
"""Test update_firmware raises ValueError when firmware_url is empty or invalid type."""
|
|
1170
1217
|
test_charger._config["version"] = "4.1.7"
|
|
1171
|
-
with
|
|
1172
|
-
|
|
1218
|
+
with caplog.at_level(logging.DEBUG):
|
|
1219
|
+
with pytest.raises(ValueError):
|
|
1220
|
+
await test_charger.update_firmware(firmware_url="")
|
|
1221
|
+
assert "Invalid firmware_url: " in caplog.text
|
|
1173
1222
|
|
|
1174
|
-
|
|
1175
|
-
|
|
1223
|
+
with pytest.raises(ValueError):
|
|
1224
|
+
await test_charger.update_firmware(firmware_url=" ")
|
|
1225
|
+
assert "Invalid firmware_url: " in caplog.text
|
|
1176
1226
|
|
|
1177
|
-
|
|
1178
|
-
|
|
1227
|
+
with pytest.raises(ValueError):
|
|
1228
|
+
await test_charger.update_firmware(firmware_url=123) # type: ignore
|
|
1229
|
+
assert "Invalid firmware_url: 123" in caplog.text
|
|
1179
1230
|
|
|
1180
1231
|
|
|
1181
1232
|
async def test_update_firmware_unsupported(test_charger):
|
|
@@ -1183,3 +1234,100 @@ async def test_update_firmware_unsupported(test_charger):
|
|
|
1183
1234
|
test_charger._config["version"] = "4.1.2"
|
|
1184
1235
|
with pytest.raises(UnsupportedFeature):
|
|
1185
1236
|
await test_charger.update_firmware(firmware_url="http://url")
|
|
1237
|
+
|
|
1238
|
+
|
|
1239
|
+
async def test_update_firmware_error_response(test_charger, mock_aioclient, caplog):
|
|
1240
|
+
"""Test update_firmware doesn't set ota_update on error responses."""
|
|
1241
|
+
test_charger._config["version"] = "4.1.7"
|
|
1242
|
+
test_charger._status = {}
|
|
1243
|
+
mock_aioclient.post(
|
|
1244
|
+
"http://openevse.test.tld/update",
|
|
1245
|
+
status=200,
|
|
1246
|
+
body='{"msg":"error"}',
|
|
1247
|
+
)
|
|
1248
|
+
with caplog.at_level(logging.DEBUG):
|
|
1249
|
+
response = await test_charger.update_firmware(
|
|
1250
|
+
firmware_url="http://github.com/release.bin"
|
|
1251
|
+
)
|
|
1252
|
+
assert response == {"msg": "error"}
|
|
1253
|
+
assert (
|
|
1254
|
+
"Firmware update response did not indicate start: {'msg': 'error'}"
|
|
1255
|
+
in caplog.text
|
|
1256
|
+
)
|
|
1257
|
+
assert test_charger.ota_update is False
|
|
1258
|
+
|
|
1259
|
+
|
|
1260
|
+
async def test_update_firmware_assets_invalid_type(
|
|
1261
|
+
test_charger, mock_aioclient, caplog
|
|
1262
|
+
):
|
|
1263
|
+
"""Test update_firmware handles non-list assets gracefully."""
|
|
1264
|
+
test_charger._config = {"version": "4.1.7", "buildenv": "openevse_esp32-gateway"}
|
|
1265
|
+
|
|
1266
|
+
# Mock GitHub releases but with assets as a dict (invalid type)
|
|
1267
|
+
github_response = {
|
|
1268
|
+
"tag_name": "v4.1.2",
|
|
1269
|
+
"body": "release notes",
|
|
1270
|
+
"html_url": "https://github.com/OpenEVSE/releases/v4.1.2",
|
|
1271
|
+
"assets": {"name": "not_a_list"},
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
mock_aioclient.get(
|
|
1275
|
+
"https://api.github.com/repos/OpenEVSE/ESP32_WiFi_V4.x/releases/latest",
|
|
1276
|
+
status=200,
|
|
1277
|
+
body=json.dumps(github_response),
|
|
1278
|
+
)
|
|
1279
|
+
|
|
1280
|
+
with caplog.at_level(logging.DEBUG):
|
|
1281
|
+
with pytest.raises(
|
|
1282
|
+
RuntimeError,
|
|
1283
|
+
match=r"Could not resolve latest firmware download URL from GitHub\.",
|
|
1284
|
+
):
|
|
1285
|
+
await test_charger.update_firmware()
|
|
1286
|
+
assert "Invalid GitHub assets payload: {'name': 'not_a_list'}" in caplog.text
|
|
1287
|
+
|
|
1288
|
+
|
|
1289
|
+
async def test_update_firmware_assets_invalid_item(
|
|
1290
|
+
test_charger, mock_aioclient, caplog
|
|
1291
|
+
):
|
|
1292
|
+
"""Test update_firmware handles non-mapping assets gracefully."""
|
|
1293
|
+
test_charger._config = {"version": "4.1.7", "buildenv": "openevse_esp32-gateway"}
|
|
1294
|
+
|
|
1295
|
+
# Mock GitHub releases with assets list containing non-mapping elements (e.g., a string)
|
|
1296
|
+
github_response = {
|
|
1297
|
+
"tag_name": "v4.1.2",
|
|
1298
|
+
"body": "release notes",
|
|
1299
|
+
"html_url": "https://github.com/OpenEVSE/releases/v4.1.2",
|
|
1300
|
+
"assets": [
|
|
1301
|
+
"invalid_asset_string",
|
|
1302
|
+
{
|
|
1303
|
+
"name": "openevse_esp32-gateway.bin",
|
|
1304
|
+
"browser_download_url": "http://url",
|
|
1305
|
+
},
|
|
1306
|
+
],
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
mock_aioclient.get(
|
|
1310
|
+
"https://api.github.com/repos/OpenEVSE/ESP32_WiFi_V4.x/releases/latest",
|
|
1311
|
+
status=200,
|
|
1312
|
+
body=json.dumps(github_response),
|
|
1313
|
+
)
|
|
1314
|
+
|
|
1315
|
+
mock_aioclient.post(
|
|
1316
|
+
"http://openevse.test.tld/update",
|
|
1317
|
+
status=200,
|
|
1318
|
+
body='{"msg":"started"}',
|
|
1319
|
+
)
|
|
1320
|
+
|
|
1321
|
+
with caplog.at_level(logging.DEBUG):
|
|
1322
|
+
response = await test_charger.update_firmware()
|
|
1323
|
+
assert response == {"msg": "started"}
|
|
1324
|
+
assert "Found matching firmware asset: http://url" in caplog.text
|
|
1325
|
+
|
|
1326
|
+
|
|
1327
|
+
async def test_update_firmware_bytes_empty(test_charger, caplog):
|
|
1328
|
+
"""Test update_firmware raises ValueError when empty firmware_bytes are provided."""
|
|
1329
|
+
test_charger._config["version"] = "4.1.7"
|
|
1330
|
+
with caplog.at_level(logging.DEBUG):
|
|
1331
|
+
with pytest.raises(ValueError):
|
|
1332
|
+
await test_charger.update_firmware(firmware_bytes=b"")
|
|
1333
|
+
assert "Empty firmware bytes provided" in caplog.text
|
|
@@ -48,7 +48,15 @@ async def test_set_override(
|
|
|
48
48
|
with caplog.at_level(logging.DEBUG):
|
|
49
49
|
status = await test_charger.set_override("active")
|
|
50
50
|
assert status == {"msg": "OK"}
|
|
51
|
-
assert "
|
|
51
|
+
assert mock_aioclient.requests[-1][2]["json"] == {
|
|
52
|
+
"state": "active",
|
|
53
|
+
"charge_current": 0,
|
|
54
|
+
"max_current": 0,
|
|
55
|
+
"energy_limit": 0,
|
|
56
|
+
"time_limit": 0,
|
|
57
|
+
"auto_release": True,
|
|
58
|
+
}
|
|
59
|
+
caplog.clear()
|
|
52
60
|
|
|
53
61
|
mock_aioclient.post(
|
|
54
62
|
TEST_URL_OVERRIDE,
|
|
@@ -56,44 +64,79 @@ async def test_set_override(
|
|
|
56
64
|
body='{"msg": "OK"}',
|
|
57
65
|
)
|
|
58
66
|
status = await test_charger.set_override("active", 30)
|
|
59
|
-
assert "
|
|
67
|
+
assert mock_aioclient.requests[-1][2]["json"] == {
|
|
68
|
+
"state": "active",
|
|
69
|
+
"charge_current": 30,
|
|
70
|
+
"max_current": 0,
|
|
71
|
+
"energy_limit": 0,
|
|
72
|
+
"time_limit": 0,
|
|
73
|
+
"auto_release": True,
|
|
74
|
+
}
|
|
75
|
+
caplog.clear()
|
|
76
|
+
|
|
60
77
|
mock_aioclient.post(
|
|
61
78
|
TEST_URL_OVERRIDE,
|
|
62
79
|
status=200,
|
|
63
80
|
body='{"msg": "OK"}',
|
|
64
81
|
)
|
|
65
82
|
status = await test_charger.set_override(charge_current=30)
|
|
66
|
-
assert "
|
|
83
|
+
assert mock_aioclient.requests[-1][2]["json"] == {
|
|
84
|
+
"state": "active",
|
|
85
|
+
"charge_current": 30,
|
|
86
|
+
"max_current": 0,
|
|
87
|
+
"energy_limit": 0,
|
|
88
|
+
"time_limit": 0,
|
|
89
|
+
"auto_release": True,
|
|
90
|
+
}
|
|
91
|
+
caplog.clear()
|
|
92
|
+
|
|
67
93
|
mock_aioclient.post(
|
|
68
94
|
TEST_URL_OVERRIDE,
|
|
69
95
|
status=200,
|
|
70
96
|
body='{"msg": "OK"}',
|
|
71
97
|
)
|
|
72
98
|
status = await test_charger.set_override("active", 30, 32)
|
|
73
|
-
assert
|
|
74
|
-
"
|
|
75
|
-
|
|
76
|
-
|
|
99
|
+
assert mock_aioclient.requests[-1][2]["json"] == {
|
|
100
|
+
"state": "active",
|
|
101
|
+
"charge_current": 30,
|
|
102
|
+
"max_current": 32,
|
|
103
|
+
"energy_limit": 0,
|
|
104
|
+
"time_limit": 0,
|
|
105
|
+
"auto_release": True,
|
|
106
|
+
}
|
|
107
|
+
caplog.clear()
|
|
108
|
+
|
|
77
109
|
mock_aioclient.post(
|
|
78
110
|
TEST_URL_OVERRIDE,
|
|
79
111
|
status=200,
|
|
80
112
|
body='{"msg": "OK"}',
|
|
81
113
|
)
|
|
82
114
|
status = await test_charger.set_override("active", 30, 32, 2000)
|
|
83
|
-
assert
|
|
84
|
-
"
|
|
85
|
-
|
|
86
|
-
|
|
115
|
+
assert mock_aioclient.requests[-1][2]["json"] == {
|
|
116
|
+
"state": "active",
|
|
117
|
+
"charge_current": 30,
|
|
118
|
+
"max_current": 32,
|
|
119
|
+
"energy_limit": 2000,
|
|
120
|
+
"time_limit": 0,
|
|
121
|
+
"auto_release": True,
|
|
122
|
+
}
|
|
123
|
+
caplog.clear()
|
|
124
|
+
|
|
87
125
|
mock_aioclient.post(
|
|
88
126
|
TEST_URL_OVERRIDE,
|
|
89
127
|
status=200,
|
|
90
128
|
body='{"msg": "OK"}',
|
|
91
129
|
)
|
|
92
130
|
status = await test_charger.set_override("active", 30, 32, 2000, 5000)
|
|
93
|
-
assert
|
|
94
|
-
"
|
|
95
|
-
|
|
96
|
-
|
|
131
|
+
assert mock_aioclient.requests[-1][2]["json"] == {
|
|
132
|
+
"state": "active",
|
|
133
|
+
"charge_current": 30,
|
|
134
|
+
"max_current": 32,
|
|
135
|
+
"energy_limit": 2000,
|
|
136
|
+
"time_limit": 5000,
|
|
137
|
+
"auto_release": True,
|
|
138
|
+
}
|
|
139
|
+
caplog.clear()
|
|
97
140
|
|
|
98
141
|
with pytest.raises(ValueError):
|
|
99
142
|
with caplog.at_level(logging.DEBUG):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/python_openevse_http.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/python_openevse_http.egg-info/not-zip-safe
RENAMED
|
File without changes
|
{python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/python_openevse_http.egg-info/requires.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/tests/fixtures/v2_json/config.json
RENAMED
|
File without changes
|
{python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/tests/fixtures/v2_json/status.json
RENAMED
|
File without changes
|
|
File without changes
|
{python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/tests/fixtures/v4_json/config-broken.json
RENAMED
|
File without changes
|
{python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/tests/fixtures/v4_json/config-dev.json
RENAMED
|
File without changes
|
|
File without changes
|
{python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/tests/fixtures/v4_json/config-new.json
RENAMED
|
File without changes
|
|
File without changes
|
{python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/tests/fixtures/v4_json/config.json
RENAMED
|
File without changes
|
{python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/tests/fixtures/v4_json/schedule.json
RENAMED
|
File without changes
|
{python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/tests/fixtures/v4_json/status-broken.json
RENAMED
|
File without changes
|
{python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/tests/fixtures/v4_json/status-new.json
RENAMED
|
File without changes
|
{python_openevse_http-0.4.1 → python_openevse_http-0.4.3}/tests/fixtures/v4_json/status.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|