Skip to content

Commit f61adcb

Browse files
committed
improve override state validation and refine RAPI command response handling
1 parent 2893cc3 commit f61adcb

6 files changed

Lines changed: 69 additions & 43 deletions

File tree

openevsehttp/override.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,19 @@ async def set(
4747
url = f"{self._evse.url}override"
4848

4949
data: dict[str, Any] = await self.get()
50-
if not isinstance(data, dict) or data.get("ok") is False or "state" not in data:
50+
required_keys = [
51+
"state",
52+
"charge_current",
53+
"max_current",
54+
"energy_limit",
55+
"time_limit",
56+
"auto_release",
57+
]
58+
if (
59+
not isinstance(data, dict)
60+
or data.get("ok") is False
61+
or not all(key in data for key in required_keys)
62+
):
5163
_LOGGER.error("Failed to retrieve current override state: %s", data)
5264
raise UnknownError
5365

openevsehttp/requester.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,10 @@ async def _process_request_with_session(
144144
any(key in message for key in UPDATE_TRIGGERS)
145145
or message.get("msg") in ("done", "no change", "OK")
146146
or message.get("ret") == "$OK"
147-
or message.get("cmd") is not None
147+
or (
148+
message.get("cmd") is not None
149+
and message.get("ret", "").startswith("$OK")
150+
)
148151
)
149152
):
150153
if self._invoking_callback:

tests/test_client.py

Lines changed: 34 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,7 @@ async def test_set_current(test_charger, mock_aioclient, caplog):
618618
"http://openevse.test.tld/override",
619619
status=200,
620620
body=json.dumps(value),
621+
repeat=True,
621622
)
622623
mock_aioclient.post(
623624
"http://openevse.test.tld/override",
@@ -800,7 +801,7 @@ async def test_set_divertmode_broken(test_charger_broken):
800801

801802

802803
async def test_set_divertmode_unknown_semver(test_charger_unknown_semver, caplog):
803-
# 2. Line 40-41: Non-semver version
804+
# Handle non-semver firmware versions gracefully
804805
test_charger_unknown_semver._config = {"version": "not-semver"}
805806
with caplog.at_level(logging.DEBUG):
806807
with pytest.raises(UnsupportedFeature):
@@ -1701,17 +1702,17 @@ async def noop():
17011702
await asyncio.gather(*charger.tasks, return_exceptions=True)
17021703
charger.tasks = set()
17031704

1704-
# Now mock it as connected to finally hit AlreadyListening at line 191 (was 181)
1705+
# Mock as connected to trigger AlreadyListening branch
17051706
charger.websocket.state = "connected"
17061707
charger._ws_listening = True
17071708
with pytest.raises(AlreadyListening):
17081709
await charger.ws_start()
17091710

1710-
# 6. Lines 258 in ws_disconnect: Idempotence return on a FRESH instance
1711+
# Verify idempotence on a fresh instance
17111712
charger_fresh = OpenEVSE(SERVER_URL)
1712-
await charger_fresh.ws_disconnect() # Hits line 258 immediately
1713+
await charger_fresh.ws_disconnect() # Disconnect immediately
17131714

1714-
# 7. Lines 556-557: set_led_brightness error
1715+
# Verify led_brightness error handling
17151716
charger._config["version"] = "4.2.0"
17161717
mock_aioclient.post(TEST_URL_CONFIG, status=200, body='{"msg": "failed"}')
17171718
with caplog.at_level(logging.ERROR):
@@ -1720,21 +1721,21 @@ async def noop():
17201721
assert "Problem issuing command. Response: {'msg': 'failed'}" in caplog.text
17211722
caplog.clear()
17221723

1723-
# 8. Lines 697-699: state property with invalid state_idx
1724+
# Verify state property invalid index handling
17241725
charger._status["state"] = "invalid"
17251726
with caplog.at_level(logging.DEBUG):
17261727
assert charger.state == "unknown"
17271728
assert "Invalid state value: invalid" in caplog.text
17281729

1729-
# 9. Lines 716-718: status property branches
1730+
# Verify status property branch handling
17301731
charger._status = {"status": "present"}
17311732
assert charger.status == "present"
17321733
charger._status = {"state": 3} # Charging
17331734
assert charger.status == "charging"
17341735

17351736

17361737
async def test_set_divert_mode_error_coverage(mock_aioclient, caplog):
1737-
"""Line 547 coverage (set_divert_mode error path)."""
1738+
"""Verify set_divert_mode error handling."""
17381739
charger = OpenEVSE(SERVER_URL)
17391740
charger._config["version"] = "4.2.2"
17401741

@@ -1813,18 +1814,18 @@ async def test_client_none_safeguards(mock_aioclient):
18131814
"""Test safety paths when websocket or config is None."""
18141815
charger = OpenEVSE(SERVER_URL)
18151816

1816-
# 1. ws_state when websocket is None (line 298)
1817+
# Verify ws_state when no websocket set
18171818
charger.websocket = None
18181819
assert charger.ws_state == "stopped"
18191820

1820-
# 2. divert_mode when _config is None (line 464)
1821+
# Verify divert_mode when no config available
18211822
charger._config = None
18221823
with pytest.raises(UnsupportedFeature):
18231824
await charger.divert_mode()
18241825

18251826

18261827
async def test_update_failure_cache_preservation(mock_aioclient):
1827-
"""Test that update() preserves cache on failure (Lines 133-134)."""
1828+
"""Verify that update() preserves cache data on network failure."""
18281829
charger = OpenEVSE(SERVER_URL)
18291830
# Initial success to establish a state
18301831
mock_aioclient.get(
@@ -1878,7 +1879,7 @@ async def test_set_current_transport_fail(caplog):
18781879

18791880

18801881
async def test_websocket_update_exception_handling(caplog):
1881-
"""Test update failure during websocket push (Lines 260-261)."""
1882+
"""Verify update failure during websocket status push."""
18821883
charger = OpenEVSE(SERVER_URL)
18831884
charger._config["version"] = "4.0.0"
18841885
trigger_key = next(iter(UPDATE_TRIGGERS))
@@ -1902,12 +1903,12 @@ async def test_websocket_non_mapping_payload(caplog):
19021903

19031904

19041905
async def test_set_current_rapi_dict_error(mock_aioclient):
1905-
"""Test set_current handling of RAPI dict error (Lines 524-525)."""
1906+
"""Verify set_current handling of RAPI dict error."""
19061907
charger = OpenEVSE(SERVER_URL)
19071908
# Force RAPI branch
19081909
charger._config["version"] = "2.9.0"
19091910

1910-
# requester.py Line 159 covered here too (returning dict if ok=False)
1911+
# requester.py handles returning dict if ok=False
19111912
mock_aioclient.post(
19121913
TEST_URL_RAPI, status=200, body='{"ok": false, "msg": "transport error"}'
19131914
)
@@ -1916,7 +1917,7 @@ async def test_set_current_rapi_dict_error(mock_aioclient):
19161917

19171918

19181919
async def test_restart_evse_rapi_dict_error(mock_aioclient):
1919-
"""Test restart_evse handling of RAPI dict error (Lines 580-581)."""
1920+
"""Verify restart_evse handling of RAPI dict error."""
19201921
charger = OpenEVSE(SERVER_URL)
19211922
# Force RAPI branch
19221923
charger._config["version"] = "4.1.0"
@@ -2131,20 +2132,20 @@ async def test_client_set_current_error_handling(caplog):
21312132
charger.requester = MagicMock()
21322133
charger.send_command = AsyncMock(return_value="$EX")
21332134

2134-
# Path: Missing limits for lines 522-525
2135+
# Handle missing hard current limits configuration
21352136
charger._version_check = lambda v: True
21362137
charger._config = {"min_current_hard": None, "max_current_hard": 40}
21372138
with caplog.at_level(logging.ERROR):
21382139
with pytest.raises(RuntimeError, match="Hard current limits are missing"):
21392140
await charger.set_current(10)
21402141
assert "Missing current limits in config" in caplog.text
21412142

2142-
# Path: Invalid value for lines 528-534
2143+
# Handle invalid value out of hard limits range
21432144
charger._config = {"min_current_hard": 6, "max_current_hard": 40}
21442145
with pytest.raises(ValueError):
21452146
await charger.set_current(5)
21462147

2147-
# Path: RAPI failure for lines 552-555
2148+
# Handle RAPI failure during current set
21482149
charger._version_check = lambda v: False
21492150
assert await charger.set_current(10) is False
21502151

@@ -2167,29 +2168,29 @@ async def test_client_property_edge_cases():
21672168
"""Test property accessors with malformed or missing status data."""
21682169
charger = OpenEVSE("openevse.test.tld")
21692170

2170-
# State with invalid index for lines 829-831
2171+
# Handle state fallback for invalid index
21712172
charger._status = {"state": "invalid"}
21722173
assert charger.state == "unknown"
21732174

2174-
# Wifi signal with invalid data for lines 849, 852-853
2175+
# Handle Wifi signal with invalid data
21752176
charger._status = {"srssi": "invalid"}
21762177
assert charger.wifi_signal is None
21772178
charger._status = {}
21782179
assert charger.wifi_signal is None
21792180

2180-
# Ambient temp with bools and fallback for line 885
2181+
# Handle ambient temp with boolean data fallback
21812182
charger._status = {"temp": True, "temp1": False}
21822183
assert charger.ambient_temperature is None
21832184

2184-
# Service level edge cases for lines 727, 730-731
2185+
# Handle service level edge cases
21852186
charger._config = {"service": "invalid"}
21862187
assert charger.service_level is None
21872188
charger._config = {}
21882189
assert charger.service_level is None
21892190

21902191

21912192
async def test_client_test_and_get_error():
2192-
"""Test test_and_get raising UnknownError for lines 156-157."""
2193+
"""Verify that test_and_get raises UnknownError on failure."""
21932194
charger = OpenEVSE("openevse.test.tld")
21942195
charger.requester = MagicMock()
21952196
charger.requester.process_request = AsyncMock(return_value={"ok": False})
@@ -2198,7 +2199,7 @@ async def test_client_test_and_get_error():
21982199

21992200

22002201
async def test_client_update_failure_path(caplog):
2201-
"""Test update method error path for line 146."""
2202+
"""Verify update method exception handling."""
22022203
charger = OpenEVSE("openevse.test.tld")
22032204
charger.requester = MagicMock()
22042205
charger.requester.process_request = AsyncMock(side_effect=Exception("Failed"))
@@ -2207,7 +2208,7 @@ async def test_client_update_failure_path(caplog):
22072208

22082209

22092210
async def test_client_ws_disconnect_task_cleanup():
2210-
"""Test task cleanup during websocket disconnect for lines 290-294."""
2211+
"""Verify task cleanup during websocket disconnection."""
22112212
charger = OpenEVSE("openevse.test.tld")
22122213
charger.websocket = MagicMock()
22132214
charger.websocket.close = AsyncMock()
@@ -2237,7 +2238,7 @@ async def test_client_rapi_fallback_errors():
22372238

22382239

22392240
async def test_client_restart_wifi_errors():
2240-
"""Test restart_wifi for lines 586-587, 592-593."""
2241+
"""Verify restart_wifi error handling."""
22412242
charger = OpenEVSE("openevse.test.tld")
22422243
charger.requester = MagicMock()
22432244
charger.requester.process_request = AsyncMock(return_value={"ok": False})
@@ -2252,7 +2253,7 @@ async def test_client_restart_wifi_errors():
22522253

22532254

22542255
async def test_client_restart_evse_errors():
2255-
"""Test restart_evse for lines 603-604."""
2256+
"""Verify restart_evse error handling."""
22562257
charger = OpenEVSE("openevse.test.tld")
22572258
charger._config["version"] = "5.0.0"
22582259
charger.requester = MagicMock()
@@ -2262,7 +2263,7 @@ async def test_client_restart_evse_errors():
22622263

22632264

22642265
async def test_client_led_brightness_errors():
2265-
"""Test set_led_brightness for lines 638-639, 643-644."""
2266+
"""Verify set_led_brightness error handling."""
22662267
charger = OpenEVSE("openevse.test.tld")
22672268
charger._config["version"] = "4.1.0"
22682269
charger.requester = MagicMock()
@@ -2278,7 +2279,7 @@ async def test_client_led_brightness_errors():
22782279

22792280

22802281
async def test_client_divert_mode_errors():
2281-
"""Test set_divert_mode for lines 660-661."""
2282+
"""Verify set_divert_mode error handling."""
22822283
charger = OpenEVSE("openevse.test.tld")
22832284
charger.requester = MagicMock()
22842285
charger.requester.process_request = AsyncMock(return_value={"ok": False})
@@ -2287,7 +2288,7 @@ async def test_client_divert_mode_errors():
22872288

22882289

22892290
async def test_client_ws_start_active_tasks():
2290-
"""Test ws_start with active tasks for line 203."""
2291+
"""Verify ws_start behavior when tasks are already active."""
22912292
charger = OpenEVSE("openevse.test.tld")
22922293
charger._loop = asyncio.get_running_loop()
22932294
charger.websocket = MagicMock()
@@ -2307,7 +2308,7 @@ async def test_client_ws_start_active_tasks():
23072308

23082309

23092310
async def test_client_shaper_active_coverage():
2310-
"""Test shaper_active property for coverage of lines 1044-1049."""
2311+
"""Verify shaper_active property with various data types and fallbacks."""
23112312
charger = OpenEVSE("openevse.test.tld")
23122313

23132314
# bool path (1044)
@@ -2330,14 +2331,14 @@ async def test_client_shaper_active_coverage():
23302331

23312332

23322333
async def test_client_lifecycle_branches():
2333-
"""Test internal lifecycle branches for coverage of lines 211-217."""
2334+
"""Verify internal lifecycle branch handling."""
23342335
charger = OpenEVSE("openevse.test.tld")
23352336
charger._loop = asyncio.get_running_loop()
23362337
charger.websocket = MagicMock()
23372338
charger.websocket.listen = AsyncMock()
23382339
charger.websocket.keepalive = AsyncMock()
23392340

2340-
# Trigger line 205-210 by having no tasks
2341+
# Initialize tasks when none exist
23412342
charger.tasks = None
23422343
await charger.ws_start()
23432344
assert len(charger.tasks) == 2

tests/test_override.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,13 @@
1010

1111
from openevsehttp.__main__ import OpenEVSE
1212
from openevsehttp.exceptions import UnknownError, UnsupportedFeature
13-
from tests.const import SERVER_URL, TEST_URL_OVERRIDE, TEST_URL_RAPI
13+
from tests.const import (
14+
SERVER_URL,
15+
TEST_URL_CONFIG,
16+
TEST_URL_OVERRIDE,
17+
TEST_URL_RAPI,
18+
TEST_URL_STATUS,
19+
)
1420

1521
pytestmark = pytest.mark.asyncio
1622

@@ -398,9 +404,14 @@ async def test_clear_override(
398404
status=200,
399405
body='{"msg": "OK"}',
400406
)
407+
# Mock refresh calls
408+
mock_aioclient.get(TEST_URL_STATUS, status=200, body='{"state": 1}')
409+
mock_aioclient.get(TEST_URL_CONFIG, status=200, body='{"version": "4.1.0"}')
410+
401411
with caplog.at_level(logging.DEBUG):
402412
await test_charger.clear_override()
403413
assert "Clear response: OK" in caplog.text
414+
assert "Forced full refresh." in caplog.text
404415

405416
await test_charger_legacy.update()
406417
with caplog.at_level(logging.DEBUG):

tests/test_requester.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -681,7 +681,7 @@ async def test_process_request_post_no_callback(mock_aioclient):
681681
req = Requester(SERVER_URL)
682682
# req._update_callback is None by default
683683
await req.process_request(TEST_URL_CONFIG, method="post", data={})
684-
# Should not crash on line 125/127
684+
# Verify no crash when _update_callback is None
685685

686686

687687
async def test_json_list_response():

tests/test_websocket.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -292,14 +292,13 @@ async def test_keepalive_send_exceptions(ws_client_auth):
292292
ws_client_auth._client.send_json.side_effect = ValueError("Value err")
293293
await ws_client_auth.keepalive()
294294

295-
# RuntimeError
296-
# Code sets state to STATE_DISCONNECTED on RuntimeError (line 172)
295+
# Handle state transition on RuntimeError
297296
ws_client_auth._client.send_json.side_effect = RuntimeError("Runtime err")
298297
await ws_client_auth.keepalive()
299298
assert ws_client_auth.state == STATE_DISCONNECTED
300299

301300
# Generic Exception
302-
# Code sets state to STATE_DISCONNECTED on generic Exception (line 175)
301+
# Handle state transition on generic Exception
303302
ws_client_auth._client.send_json.side_effect = Exception("Generic err")
304303
await ws_client_auth.keepalive()
305304
assert ws_client_auth.state == STATE_DISCONNECTED
@@ -330,8 +329,8 @@ async def test_state_setter_threadsafe_fallback(ws_client):
330329

331330
@pytest.mark.asyncio
332331
async def test_websocket_coverage_gaps(ws_client):
333-
"""Test remaining lines in websocket.py."""
334-
# 1. Line 102: break when STATE_STOPPED
332+
"""Test specific coverage gaps in websocket.py."""
333+
# Case: break loop when status becomes STOPPED
335334
msg = MagicMock()
336335
msg.type = aiohttp.WSMsgType.TEXT
337336
msg.json.return_value = {"key": "value"}
@@ -355,7 +354,7 @@ async def async_iter_stop():
355354
await ws_client.running()
356355
assert ws_client.state == STATE_STOPPED
357356

358-
# 2. Line 109: "pong" in msg.keys()
357+
# Case: handle "pong" messages
359358
msg_pong = MagicMock()
360359
msg_pong.type = aiohttp.WSMsgType.TEXT
361360
msg_pong.json.return_value = {"pong": 1}

0 commit comments

Comments
 (0)