python-openevse-http 0.4.2__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.2/python_openevse_http.egg-info → python_openevse_http-0.4.3}/PKG-INFO +1 -1
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/openevsehttp/commands.py +72 -2
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3/python_openevse_http.egg-info}/PKG-INFO +1 -1
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/requirements_test.txt +1 -2
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/tests/conftest.py +176 -3
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/tests/test_commands.py +154 -25
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/tests/test_managers.py +58 -15
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/.github/dependabot.yml +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/.github/release-drafter.yml +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/.github/workflows/autolabeler.yml +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/.github/workflows/links.yml +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/.github/workflows/publish-to-pypi.yml +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/.github/workflows/release-drafter.yml +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/.github/workflows/test.yml +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/.gitignore +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/.pre-commit-config.yaml +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/.yamllint +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/EXTERNAL_SESSION.md +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/LICENSE +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/README.md +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/codecov.yml +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/example_external_session.py +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/openevsehttp/__init__.py +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/openevsehttp/__main__.py +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/openevsehttp/client.py +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/openevsehttp/const.py +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/openevsehttp/exceptions.py +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/openevsehttp/managers.py +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/openevsehttp/properties.py +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/openevsehttp/sensors.py +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/openevsehttp/utils.py +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/openevsehttp/websocket.py +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/pyproject.toml +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/python_openevse_http.egg-info/SOURCES.txt +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/python_openevse_http.egg-info/dependency_links.txt +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/python_openevse_http.egg-info/not-zip-safe +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/python_openevse_http.egg-info/requires.txt +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/python_openevse_http.egg-info/top_level.txt +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/requirements.txt +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/requirements_lint.txt +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/setup.cfg +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/setup.py +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/tests/__init__.py +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/tests/common.py +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/tests/fixtures/github_v2.json +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/tests/fixtures/github_v4.json +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/tests/fixtures/v2_json/config.json +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/tests/fixtures/v2_json/status.json +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/tests/fixtures/v4_json/config-broken-semver.json +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/tests/fixtures/v4_json/config-broken.json +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/tests/fixtures/v4_json/config-dev.json +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/tests/fixtures/v4_json/config-extra-version.json +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/tests/fixtures/v4_json/config-new.json +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/tests/fixtures/v4_json/config-unknown-semver.json +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/tests/fixtures/v4_json/config.json +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/tests/fixtures/v4_json/schedule.json +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/tests/fixtures/v4_json/status-broken.json +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/tests/fixtures/v4_json/status-new.json +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/tests/fixtures/v4_json/status.json +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/tests/fixtures/websocket.json +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/tests/test_client.py +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/tests/test_external_session.py +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/tests/test_main_edge_cases.py +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/tests/test_mixins.py +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/tests/test_properties.py +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/tests/test_sensors.py +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/tests/test_shaper.py +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/tests/test_websocket.py +0 -0
- {python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/tox.ini +0 -0
|
@@ -53,7 +53,12 @@ class CommandsMixin:
|
|
|
53
53
|
normalized.get("msg") == "started"
|
|
54
54
|
or normalized.get("msg") in SUCCESS_ANSWERS
|
|
55
55
|
):
|
|
56
|
+
_LOGGER.debug("Firmware update started, setting ota_update flag.")
|
|
56
57
|
self._status["ota_update"] = 1
|
|
58
|
+
else:
|
|
59
|
+
_LOGGER.debug(
|
|
60
|
+
"Firmware update response did not indicate start: %s", normalized
|
|
61
|
+
)
|
|
57
62
|
|
|
58
63
|
async def get_schedule(self) -> Mapping[str, Any] | list[Any]:
|
|
59
64
|
"""Return the current schedule."""
|
|
@@ -131,7 +136,12 @@ class CommandsMixin:
|
|
|
131
136
|
time_limit: int | None = None,
|
|
132
137
|
auto_release: bool | None = None,
|
|
133
138
|
) -> Any:
|
|
134
|
-
"""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
|
+
"""
|
|
135
145
|
if not self._version_check("4.0.1"):
|
|
136
146
|
_LOGGER.debug("Feature not supported for older firmware.")
|
|
137
147
|
raise UnsupportedFeature
|
|
@@ -149,6 +159,18 @@ class CommandsMixin:
|
|
|
149
159
|
raise ValueError
|
|
150
160
|
|
|
151
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
|
+
|
|
152
174
|
if auto_release is not None:
|
|
153
175
|
data["auto_release"] = auto_release
|
|
154
176
|
|
|
@@ -381,6 +403,7 @@ class CommandsMixin:
|
|
|
381
403
|
url = f"{base_url}ESP32_WiFi_V4.x/releases/latest"
|
|
382
404
|
else:
|
|
383
405
|
url = f"{base_url}ESP8266_WiFi_v2.x/releases/latest"
|
|
406
|
+
_LOGGER.debug("Firmware check URL: %s", url)
|
|
384
407
|
except AwesomeVersionCompareException:
|
|
385
408
|
_LOGGER.debug("Non-semver firmware version detected.")
|
|
386
409
|
return None
|
|
@@ -412,6 +435,7 @@ class CommandsMixin:
|
|
|
412
435
|
method,
|
|
413
436
|
)
|
|
414
437
|
async with http_method(url) as resp:
|
|
438
|
+
_LOGGER.debug("Firmware check response status: %d", resp.status)
|
|
415
439
|
if resp.status != 200:
|
|
416
440
|
return None
|
|
417
441
|
message = await resp.text()
|
|
@@ -422,19 +446,51 @@ class CommandsMixin:
|
|
|
422
446
|
return None
|
|
423
447
|
|
|
424
448
|
if not isinstance(message, dict):
|
|
449
|
+
_LOGGER.debug(
|
|
450
|
+
"Invalid JSON response type from GitHub: %s", type(message)
|
|
451
|
+
)
|
|
425
452
|
return None
|
|
426
453
|
|
|
454
|
+
_LOGGER.debug(
|
|
455
|
+
"GitHub release metadata successfully fetched for version: %s",
|
|
456
|
+
message.get("tag_name"),
|
|
457
|
+
)
|
|
458
|
+
|
|
427
459
|
# Match browser_download_url based on buildenv
|
|
428
460
|
download_url = None
|
|
429
461
|
buildenv = self._config.get("buildenv")
|
|
430
462
|
assets = message.get("assets", [])
|
|
431
463
|
|
|
432
|
-
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)
|
|
433
474
|
target_filename = f"{buildenv}.bin"
|
|
434
475
|
for asset in assets:
|
|
476
|
+
if not isinstance(asset, Mapping):
|
|
477
|
+
continue
|
|
435
478
|
if asset.get("name") == target_filename:
|
|
436
479
|
download_url = asset.get("browser_download_url")
|
|
480
|
+
_LOGGER.debug("Found matching firmware asset: %s", download_url)
|
|
437
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
|
+
)
|
|
438
494
|
|
|
439
495
|
return {
|
|
440
496
|
"latest_version": message.get("tag_name"),
|
|
@@ -461,10 +517,16 @@ class CommandsMixin:
|
|
|
461
517
|
raise UnsupportedFeature
|
|
462
518
|
|
|
463
519
|
if firmware_bytes is not None and firmware_url is not None:
|
|
520
|
+
_LOGGER.error("Cannot specify both firmware_bytes and firmware_url")
|
|
464
521
|
raise ValueError("Cannot specify both firmware_bytes and firmware_url")
|
|
465
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
|
+
|
|
466
527
|
if firmware_url is not None:
|
|
467
528
|
if not isinstance(firmware_url, str) or not firmware_url.strip():
|
|
529
|
+
_LOGGER.error("Invalid firmware_url: %s", firmware_url)
|
|
468
530
|
raise ValueError("Invalid firmware_url")
|
|
469
531
|
|
|
470
532
|
url = f"{self.url}update"
|
|
@@ -485,13 +547,20 @@ class CommandsMixin:
|
|
|
485
547
|
response = await self.process_request(
|
|
486
548
|
url=url, method="post", rapi=form_data
|
|
487
549
|
)
|
|
550
|
+
_LOGGER.debug("Firmware upload request completed. Response: %s", response)
|
|
488
551
|
self._flag_ota_if_started(response)
|
|
489
552
|
return response
|
|
490
553
|
|
|
491
554
|
# 2. Resolve URL from GitHub if not specified
|
|
492
555
|
if firmware_url is None:
|
|
556
|
+
_LOGGER.debug(
|
|
557
|
+
"No firmware URL provided. Resolving latest matching firmware from GitHub."
|
|
558
|
+
)
|
|
493
559
|
check_result = await self.firmware_check()
|
|
494
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
|
+
)
|
|
495
564
|
raise RuntimeError(
|
|
496
565
|
"Could not resolve latest firmware download URL from GitHub."
|
|
497
566
|
)
|
|
@@ -503,6 +572,7 @@ class CommandsMixin:
|
|
|
503
572
|
"Requesting OpenEVSE to download and update from: %s", firmware_url
|
|
504
573
|
)
|
|
505
574
|
response = await self.process_request(url=url, method="post", data=data)
|
|
575
|
+
_LOGGER.debug("Firmware update request completed. Response: %s", response)
|
|
506
576
|
self._flag_ota_if_started(response)
|
|
507
577
|
return response
|
|
508
578
|
|
|
@@ -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
|
|
@@ -1060,6 +1060,8 @@ 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
|
|
1063
1065
|
assert test_charger.ota_update is True
|
|
1064
1066
|
|
|
1065
1067
|
|
|
@@ -1080,6 +1082,11 @@ async def test_update_firmware_url(test_charger, mock_aioclient, caplog):
|
|
|
1080
1082
|
"Requesting OpenEVSE to download and update from: http://github.com/release.bin"
|
|
1081
1083
|
in caplog.text
|
|
1082
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
|
|
1083
1090
|
assert test_charger.ota_update is True
|
|
1084
1091
|
|
|
1085
1092
|
|
|
@@ -1120,14 +1127,43 @@ async def test_update_firmware_auto(test_charger, mock_aioclient, caplog):
|
|
|
1120
1127
|
with caplog.at_level(logging.DEBUG):
|
|
1121
1128
|
response = await test_charger.update_firmware()
|
|
1122
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
|
+
)
|
|
1123
1152
|
assert (
|
|
1124
1153
|
"Requesting OpenEVSE to download and update from: https://github.com/OpenEVSE/releases/download/v4.1.2/openevse_esp32-gateway.bin"
|
|
1125
1154
|
in caplog.text
|
|
1126
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
|
|
1127
1161
|
assert test_charger.ota_update is True
|
|
1128
1162
|
|
|
1129
1163
|
|
|
1130
|
-
async def test_update_firmware_auto_missing_buildenv(
|
|
1164
|
+
async def test_update_firmware_auto_missing_buildenv(
|
|
1165
|
+
test_charger, mock_aioclient, caplog
|
|
1166
|
+
):
|
|
1131
1167
|
"""Test update_firmware raises RuntimeError when buildenv asset is missing."""
|
|
1132
1168
|
test_charger._config = {"version": "4.1.7", "buildenv": "openevse_esp32-gateway"}
|
|
1133
1169
|
|
|
@@ -1150,35 +1186,47 @@ async def test_update_firmware_auto_missing_buildenv(test_charger, mock_aioclien
|
|
|
1150
1186
|
body=json.dumps(github_response),
|
|
1151
1187
|
)
|
|
1152
1188
|
|
|
1153
|
-
with
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
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
|
+
)
|
|
1158
1202
|
|
|
1159
1203
|
|
|
1160
|
-
async def test_update_firmware_both_provided(test_charger):
|
|
1204
|
+
async def test_update_firmware_both_provided(test_charger, caplog):
|
|
1161
1205
|
"""Test update_firmware raises ValueError when both bytes and URL are provided."""
|
|
1162
1206
|
test_charger._config["version"] = "4.1.7"
|
|
1163
|
-
with
|
|
1164
|
-
ValueError
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
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
|
|
1169
1213
|
|
|
1170
1214
|
|
|
1171
|
-
async def test_update_firmware_url_invalid(test_charger):
|
|
1215
|
+
async def test_update_firmware_url_invalid(test_charger, caplog):
|
|
1172
1216
|
"""Test update_firmware raises ValueError when firmware_url is empty or invalid type."""
|
|
1173
1217
|
test_charger._config["version"] = "4.1.7"
|
|
1174
|
-
with
|
|
1175
|
-
|
|
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
|
|
1176
1222
|
|
|
1177
|
-
|
|
1178
|
-
|
|
1223
|
+
with pytest.raises(ValueError):
|
|
1224
|
+
await test_charger.update_firmware(firmware_url=" ")
|
|
1225
|
+
assert "Invalid firmware_url: " in caplog.text
|
|
1179
1226
|
|
|
1180
|
-
|
|
1181
|
-
|
|
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
|
|
1182
1230
|
|
|
1183
1231
|
|
|
1184
1232
|
async def test_update_firmware_unsupported(test_charger):
|
|
@@ -1188,7 +1236,7 @@ async def test_update_firmware_unsupported(test_charger):
|
|
|
1188
1236
|
await test_charger.update_firmware(firmware_url="http://url")
|
|
1189
1237
|
|
|
1190
1238
|
|
|
1191
|
-
async def test_update_firmware_error_response(test_charger, mock_aioclient):
|
|
1239
|
+
async def test_update_firmware_error_response(test_charger, mock_aioclient, caplog):
|
|
1192
1240
|
"""Test update_firmware doesn't set ota_update on error responses."""
|
|
1193
1241
|
test_charger._config["version"] = "4.1.7"
|
|
1194
1242
|
test_charger._status = {}
|
|
@@ -1197,8 +1245,89 @@ async def test_update_firmware_error_response(test_charger, mock_aioclient):
|
|
|
1197
1245
|
status=200,
|
|
1198
1246
|
body='{"msg":"error"}',
|
|
1199
1247
|
)
|
|
1200
|
-
|
|
1201
|
-
|
|
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),
|
|
1202
1278
|
)
|
|
1203
|
-
|
|
1204
|
-
|
|
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
|
{python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/.github/workflows/publish-to-pypi.yml
RENAMED
|
File without changes
|
{python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/.github/workflows/release-drafter.yml
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
|
|
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.2 → 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.2 → python_openevse_http-0.4.3}/python_openevse_http.egg-info/not-zip-safe
RENAMED
|
File without changes
|
{python_openevse_http-0.4.2 → 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.2 → python_openevse_http-0.4.3}/tests/fixtures/v2_json/config.json
RENAMED
|
File without changes
|
{python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/tests/fixtures/v2_json/status.json
RENAMED
|
File without changes
|
|
File without changes
|
{python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/tests/fixtures/v4_json/config-broken.json
RENAMED
|
File without changes
|
{python_openevse_http-0.4.2 → 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.2 → 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.2 → python_openevse_http-0.4.3}/tests/fixtures/v4_json/config.json
RENAMED
|
File without changes
|
{python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/tests/fixtures/v4_json/schedule.json
RENAMED
|
File without changes
|
{python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/tests/fixtures/v4_json/status-broken.json
RENAMED
|
File without changes
|
{python_openevse_http-0.4.2 → python_openevse_http-0.4.3}/tests/fixtures/v4_json/status-new.json
RENAMED
|
File without changes
|
{python_openevse_http-0.4.2 → 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
|
|
File without changes
|