violet-poolController-api 0.0.29__tar.gz → 0.0.31__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.
- {violet_poolcontroller_api-0.0.29/violet_poolController_api.egg-info → violet_poolcontroller_api-0.0.31}/PKG-INFO +1 -1
- {violet_poolcontroller_api-0.0.29 → violet_poolcontroller_api-0.0.31}/pyproject.toml +1 -1
- {violet_poolcontroller_api-0.0.29 → violet_poolcontroller_api-0.0.31}/tests/test_api.py +355 -5
- {violet_poolcontroller_api-0.0.29 → violet_poolcontroller_api-0.0.31}/tests/test_mock_server.py +18 -10
- {violet_poolcontroller_api-0.0.29 → violet_poolcontroller_api-0.0.31/violet_poolController_api.egg-info}/PKG-INFO +1 -1
- {violet_poolcontroller_api-0.0.29 → violet_poolcontroller_api-0.0.31}/violet_poolcontroller_api/__init__.py +2 -0
- {violet_poolcontroller_api-0.0.29 → violet_poolcontroller_api-0.0.31}/violet_poolcontroller_api/api.py +484 -8
- {violet_poolcontroller_api-0.0.29 → violet_poolcontroller_api-0.0.31}/violet_poolcontroller_api/const_api.py +244 -11
- {violet_poolcontroller_api-0.0.29 → violet_poolcontroller_api-0.0.31}/violet_poolcontroller_api/const_devices.py +4 -2
- {violet_poolcontroller_api-0.0.29 → violet_poolcontroller_api-0.0.31}/violet_poolcontroller_api/readings.py +12 -37
- {violet_poolcontroller_api-0.0.29 → violet_poolcontroller_api-0.0.31}/LICENSE +0 -0
- {violet_poolcontroller_api-0.0.29 → violet_poolcontroller_api-0.0.31}/README.md +0 -0
- {violet_poolcontroller_api-0.0.29 → violet_poolcontroller_api-0.0.31}/setup.cfg +0 -0
- {violet_poolcontroller_api-0.0.29 → violet_poolcontroller_api-0.0.31}/tests/test_api_smoke.py +0 -0
- {violet_poolcontroller_api-0.0.29 → violet_poolcontroller_api-0.0.31}/violet_poolController_api.egg-info/SOURCES.txt +0 -0
- {violet_poolcontroller_api-0.0.29 → violet_poolcontroller_api-0.0.31}/violet_poolController_api.egg-info/dependency_links.txt +0 -0
- {violet_poolcontroller_api-0.0.29 → violet_poolcontroller_api-0.0.31}/violet_poolController_api.egg-info/requires.txt +0 -0
- {violet_poolcontroller_api-0.0.29 → violet_poolcontroller_api-0.0.31}/violet_poolController_api.egg-info/top_level.txt +0 -0
- {violet_poolcontroller_api-0.0.29 → violet_poolcontroller_api-0.0.31}/violet_poolcontroller_api/circuit_breaker.py +0 -0
- {violet_poolcontroller_api-0.0.29 → violet_poolcontroller_api-0.0.31}/violet_poolcontroller_api/parsers.py +0 -0
- {violet_poolcontroller_api-0.0.29 → violet_poolcontroller_api-0.0.31}/violet_poolcontroller_api/utils_rate_limiter.py +0 -0
- {violet_poolcontroller_api-0.0.29 → violet_poolcontroller_api-0.0.31}/violet_poolcontroller_api/utils_sanitizer.py +0 -0
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
from __future__ import annotations
|
|
22
22
|
|
|
23
|
+
from collections.abc import Mapping
|
|
23
24
|
from typing import TYPE_CHECKING, Any, Never
|
|
24
25
|
|
|
25
26
|
import aiohttp
|
|
@@ -34,6 +35,7 @@ from violet_poolcontroller_api.const_api import (
|
|
|
34
35
|
ERROR_CODES,
|
|
35
36
|
ERROR_SEVERITY_ALARM,
|
|
36
37
|
ERROR_SEVERITY_INFO,
|
|
38
|
+
ERROR_SEVERITY_REMINDER,
|
|
37
39
|
ERROR_SEVERITY_WARNING,
|
|
38
40
|
)
|
|
39
41
|
|
|
@@ -90,8 +92,8 @@ async def test_get_readings_success(
|
|
|
90
92
|
|
|
91
93
|
result = await api_client.get_readings()
|
|
92
94
|
|
|
93
|
-
assert isinstance(result,
|
|
94
|
-
assert result == mock_data
|
|
95
|
+
assert isinstance(result, Mapping)
|
|
96
|
+
assert dict(result) == mock_data
|
|
95
97
|
|
|
96
98
|
|
|
97
99
|
@pytest.mark.asyncio
|
|
@@ -289,8 +291,8 @@ async def test_get_readings_standalone_list_format(
|
|
|
289
291
|
|
|
290
292
|
result = await api_client.get_readings()
|
|
291
293
|
|
|
292
|
-
assert isinstance(result,
|
|
293
|
-
assert result == {"date": "12.04.2023", "CPU_TEMP": 45.5}
|
|
294
|
+
assert isinstance(result, Mapping)
|
|
295
|
+
assert dict(result) == {"date": "12.04.2023", "CPU_TEMP": 45.5}
|
|
294
296
|
assert api_client.dosing_standalone is True
|
|
295
297
|
|
|
296
298
|
|
|
@@ -313,7 +315,7 @@ async def test_dosing_standalone_detection_dict_format(
|
|
|
313
315
|
|
|
314
316
|
result = await standalone_api_client.get_readings()
|
|
315
317
|
|
|
316
|
-
assert isinstance(result,
|
|
318
|
+
assert isinstance(result, Mapping)
|
|
317
319
|
assert standalone_api_client.dosing_standalone is False
|
|
318
320
|
|
|
319
321
|
|
|
@@ -869,6 +871,324 @@ async def test_set_dosing_parameters(
|
|
|
869
871
|
assert result["success"] is True
|
|
870
872
|
|
|
871
873
|
|
|
874
|
+
@pytest.mark.asyncio
|
|
875
|
+
async def test_reset_blocking(
|
|
876
|
+
mock_aioresponse: aioresponses,
|
|
877
|
+
api_client: VioletPoolAPI,
|
|
878
|
+
) -> None:
|
|
879
|
+
"""Test reset_blocking clears fault blockings via GET /resetBlocking."""
|
|
880
|
+
url = "http://192.168.1.100/resetBlocking"
|
|
881
|
+
mock_aioresponse.get(url, body="OK\nBLOCKINGS_CLEARED", status=200)
|
|
882
|
+
|
|
883
|
+
result = await api_client.reset_blocking()
|
|
884
|
+
|
|
885
|
+
assert result["success"] is True
|
|
886
|
+
assert "BLOCKINGS_CLEARED" in result["response"]
|
|
887
|
+
|
|
888
|
+
|
|
889
|
+
@pytest.mark.asyncio
|
|
890
|
+
async def test_set_can_amount_adjust(
|
|
891
|
+
mock_aioresponse: aioresponses,
|
|
892
|
+
api_client: VioletPoolAPI,
|
|
893
|
+
) -> None:
|
|
894
|
+
"""Test set_can_amount (ADJUST) updates canister level via POST."""
|
|
895
|
+
url = "http://192.168.1.100/setCanAmount"
|
|
896
|
+
mock_aioresponse.post(url, body="OK\nDOS_1_CL\n25000", status=200)
|
|
897
|
+
|
|
898
|
+
result = await api_client.set_can_amount("DOS_1_CL", 25000)
|
|
899
|
+
|
|
900
|
+
assert result["success"] is True
|
|
901
|
+
# Verify form payload sent to the controller
|
|
902
|
+
sent = list(mock_aioresponse.requests.values())[0][0].kwargs["data"]
|
|
903
|
+
assert sent["action"] == "ADJUST"
|
|
904
|
+
assert sent["which"] == "DOS_1_CL"
|
|
905
|
+
assert sent["amount"] == "25000"
|
|
906
|
+
assert sent["cid"] == "1"
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
@pytest.mark.asyncio
|
|
910
|
+
async def test_set_can_amount_reset(
|
|
911
|
+
mock_aioresponse: aioresponses,
|
|
912
|
+
api_client: VioletPoolAPI,
|
|
913
|
+
) -> None:
|
|
914
|
+
"""Test set_can_amount(reset=True) sends RESET action."""
|
|
915
|
+
url = "http://192.168.1.100/setCanAmount"
|
|
916
|
+
mock_aioresponse.post(url, body="OK", status=200)
|
|
917
|
+
|
|
918
|
+
result = await api_client.set_can_amount("DOS_6_FLOC", 20000, reset=True)
|
|
919
|
+
|
|
920
|
+
assert result["success"] is True
|
|
921
|
+
sent = list(mock_aioresponse.requests.values())[0][0].kwargs["data"]
|
|
922
|
+
assert sent["action"] == "RESET"
|
|
923
|
+
assert sent["cid"] == "6"
|
|
924
|
+
|
|
925
|
+
|
|
926
|
+
@pytest.mark.asyncio
|
|
927
|
+
async def test_set_can_amount_rejects_unknown_key(
|
|
928
|
+
api_client: VioletPoolAPI,
|
|
929
|
+
) -> None:
|
|
930
|
+
"""Test set_can_amount raises on unknown dosing key."""
|
|
931
|
+
with pytest.raises(VioletPoolAPIError, match="Unknown dosing key"):
|
|
932
|
+
await api_client.set_can_amount("DOS_99_XXX", 1000)
|
|
933
|
+
|
|
934
|
+
|
|
935
|
+
@pytest.mark.asyncio
|
|
936
|
+
async def test_set_can_amount_rejects_nonpositive_amount(
|
|
937
|
+
api_client: VioletPoolAPI,
|
|
938
|
+
) -> None:
|
|
939
|
+
"""Test set_can_amount rejects zero/negative fill levels."""
|
|
940
|
+
with pytest.raises(ValueError, match="must be > 0"):
|
|
941
|
+
await api_client.set_can_amount("DOS_1_CL", 0)
|
|
942
|
+
with pytest.raises(ValueError, match="must be > 0"):
|
|
943
|
+
await api_client.set_can_amount("DOS_1_CL", -100)
|
|
944
|
+
|
|
945
|
+
|
|
946
|
+
@pytest.mark.asyncio
|
|
947
|
+
async def test_set_system_service_enable(
|
|
948
|
+
mock_aioresponse: aioresponses,
|
|
949
|
+
api_client: VioletPoolAPI,
|
|
950
|
+
) -> None:
|
|
951
|
+
"""Test set_system_service(enabled=True) hits the /enableFTP endpoint."""
|
|
952
|
+
url = "http://192.168.1.100/enableFTP"
|
|
953
|
+
mock_aioresponse.get(url, body="OK\nenableFTP", status=200)
|
|
954
|
+
|
|
955
|
+
result = await api_client.set_system_service("ftp", enabled=True)
|
|
956
|
+
|
|
957
|
+
assert result["success"] is True
|
|
958
|
+
sent_key = list(mock_aioresponse.requests.keys())[0]
|
|
959
|
+
assert str(sent_key[1]).endswith("/enableFTP")
|
|
960
|
+
|
|
961
|
+
|
|
962
|
+
@pytest.mark.asyncio
|
|
963
|
+
async def test_set_system_service_disable(
|
|
964
|
+
mock_aioresponse: aioresponses,
|
|
965
|
+
api_client: VioletPoolAPI,
|
|
966
|
+
) -> None:
|
|
967
|
+
"""Test set_system_service(enabled=False) hits /disableSSH."""
|
|
968
|
+
url = "http://192.168.1.100/disableSSH"
|
|
969
|
+
mock_aioresponse.get(url, body="OK", status=200)
|
|
970
|
+
|
|
971
|
+
result = await api_client.set_system_service("ssh", enabled=False)
|
|
972
|
+
|
|
973
|
+
assert result["success"] is True
|
|
974
|
+
sent_key = list(mock_aioresponse.requests.keys())[0]
|
|
975
|
+
assert str(sent_key[1]).endswith("/disableSSH")
|
|
976
|
+
|
|
977
|
+
|
|
978
|
+
@pytest.mark.asyncio
|
|
979
|
+
async def test_set_system_service_rejects_unknown(
|
|
980
|
+
api_client: VioletPoolAPI,
|
|
981
|
+
) -> None:
|
|
982
|
+
"""Test set_system_service raises on unknown service name."""
|
|
983
|
+
with pytest.raises(VioletPoolAPIError, match="Unknown system service"):
|
|
984
|
+
await api_client.set_system_service("telnet", enabled=True)
|
|
985
|
+
|
|
986
|
+
|
|
987
|
+
@pytest.mark.asyncio
|
|
988
|
+
async def test_get_system_services(
|
|
989
|
+
mock_aioresponse: aioresponses,
|
|
990
|
+
api_client: VioletPoolAPI,
|
|
991
|
+
) -> None:
|
|
992
|
+
"""Test get_system_services normalises raw getServiceStates response."""
|
|
993
|
+
url = "http://192.168.1.100/getServiceStates"
|
|
994
|
+
mock_aioresponse.get(
|
|
995
|
+
url,
|
|
996
|
+
payload={
|
|
997
|
+
"proftpd": 0,
|
|
998
|
+
"shairport": 1,
|
|
999
|
+
"samba": 0,
|
|
1000
|
+
"sshd": 1,
|
|
1001
|
+
"homekit": 0,
|
|
1002
|
+
"tunnel_state": 1,
|
|
1003
|
+
"support_tunnel_state": 0,
|
|
1004
|
+
"date": "29.02.2024",
|
|
1005
|
+
"time": "19:50:02",
|
|
1006
|
+
},
|
|
1007
|
+
status=200,
|
|
1008
|
+
)
|
|
1009
|
+
|
|
1010
|
+
result = await api_client.get_system_services()
|
|
1011
|
+
|
|
1012
|
+
# Alexa has no state_key and must be absent.
|
|
1013
|
+
assert "alexa" not in result
|
|
1014
|
+
assert result == {
|
|
1015
|
+
"ftp": False,
|
|
1016
|
+
"shairport": True,
|
|
1017
|
+
"samba": False,
|
|
1018
|
+
"ssh": True,
|
|
1019
|
+
"homebridge": False,
|
|
1020
|
+
"tunnel": True,
|
|
1021
|
+
"support_tunnel": False,
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
# ---------------------------------------------------------------------------
|
|
1026
|
+
# OmniTronic + RS485 + LiveTrace (P12 / P13 / P15)
|
|
1027
|
+
# ---------------------------------------------------------------------------
|
|
1028
|
+
|
|
1029
|
+
|
|
1030
|
+
@pytest.mark.asyncio
|
|
1031
|
+
async def test_set_omni_position(
|
|
1032
|
+
mock_aioresponse: aioresponses,
|
|
1033
|
+
api_client: VioletPoolAPI,
|
|
1034
|
+
) -> None:
|
|
1035
|
+
"""Test set_omni_position sends OMNI,OMNI_DC<N> via setFunctionManually."""
|
|
1036
|
+
url = "http://192.168.1.100/setFunctionManually?OMNI,OMNI_DC2,0,0"
|
|
1037
|
+
mock_aioresponse.get(url, body="OK\nOMNITRONIC\nOMNI_DC2\n", status=200)
|
|
1038
|
+
|
|
1039
|
+
result = await api_client.set_omni_position(2)
|
|
1040
|
+
|
|
1041
|
+
# The mock only matches the exact URL we registered, so a True result
|
|
1042
|
+
# proves the right URL was hit (aioresponses returns 404 otherwise).
|
|
1043
|
+
assert result["success"] is True
|
|
1044
|
+
assert "OMNITRONIC" in result["response"]
|
|
1045
|
+
# One request must have been recorded.
|
|
1046
|
+
assert len(list(mock_aioresponse.requests.values())) == 1
|
|
1047
|
+
|
|
1048
|
+
|
|
1049
|
+
@pytest.mark.asyncio
|
|
1050
|
+
async def test_set_omni_position_filtration(
|
|
1051
|
+
mock_aioresponse: aioresponses,
|
|
1052
|
+
api_client: VioletPoolAPI,
|
|
1053
|
+
) -> None:
|
|
1054
|
+
"""Position 0 (Filtration) is valid and maps to OMNI_DC0."""
|
|
1055
|
+
url = "http://192.168.1.100/setFunctionManually?OMNI,OMNI_DC0,0,0"
|
|
1056
|
+
mock_aioresponse.get(url, body="OK\nOMNITRONIC\nOMNI_DC0\n", status=200)
|
|
1057
|
+
|
|
1058
|
+
result = await api_client.set_omni_position(0)
|
|
1059
|
+
|
|
1060
|
+
assert result["success"] is True
|
|
1061
|
+
|
|
1062
|
+
|
|
1063
|
+
@pytest.mark.asyncio
|
|
1064
|
+
async def test_set_omni_position_rejects_out_of_range(
|
|
1065
|
+
api_client: VioletPoolAPI,
|
|
1066
|
+
) -> None:
|
|
1067
|
+
"""Positions outside 0-5 are rejected before the request is sent."""
|
|
1068
|
+
with pytest.raises(VioletPoolAPIError, match="Invalid OmniTronic position"):
|
|
1069
|
+
await api_client.set_omni_position(6)
|
|
1070
|
+
with pytest.raises(VioletPoolAPIError, match="Invalid OmniTronic position"):
|
|
1071
|
+
await api_client.set_omni_position(-1)
|
|
1072
|
+
|
|
1073
|
+
|
|
1074
|
+
@pytest.mark.asyncio
|
|
1075
|
+
async def test_get_rs485_pump_data(
|
|
1076
|
+
mock_aioresponse: aioresponses,
|
|
1077
|
+
api_client: VioletPoolAPI,
|
|
1078
|
+
) -> None:
|
|
1079
|
+
"""Test get_rs485_pump_data returns the pump's JSON config + live values."""
|
|
1080
|
+
url = "http://192.168.1.100/getRS485PumpData?BADU_ECO_DRIVE_II"
|
|
1081
|
+
mock_aioresponse.get(
|
|
1082
|
+
url,
|
|
1083
|
+
payload={
|
|
1084
|
+
"BRAND": "BADU",
|
|
1085
|
+
"NAME": "Eco Drive II",
|
|
1086
|
+
"pump_rs485_pwr": 450,
|
|
1087
|
+
"SLAVE_PRESENT": "YES",
|
|
1088
|
+
},
|
|
1089
|
+
status=200,
|
|
1090
|
+
)
|
|
1091
|
+
|
|
1092
|
+
result = await api_client.get_rs485_pump_data("BADU_ECO_DRIVE_II")
|
|
1093
|
+
|
|
1094
|
+
assert result["BRAND"] == "BADU"
|
|
1095
|
+
assert result["pump_rs485_pwr"] == 450
|
|
1096
|
+
assert result["SLAVE_PRESENT"] == "YES"
|
|
1097
|
+
|
|
1098
|
+
|
|
1099
|
+
@pytest.mark.asyncio
|
|
1100
|
+
async def test_get_rs485_pump_data_rejects_unknown_pump(
|
|
1101
|
+
api_client: VioletPoolAPI,
|
|
1102
|
+
) -> None:
|
|
1103
|
+
with pytest.raises(VioletPoolAPIError, match="Unknown RS485 pump name"):
|
|
1104
|
+
await api_client.get_rs485_pump_data("PENTAIR_VSF")
|
|
1105
|
+
|
|
1106
|
+
|
|
1107
|
+
@pytest.mark.asyncio
|
|
1108
|
+
async def test_set_rs485_live(
|
|
1109
|
+
mock_aioresponse: aioresponses,
|
|
1110
|
+
api_client: VioletPoolAPI,
|
|
1111
|
+
) -> None:
|
|
1112
|
+
"""Test set_rs485_live forwards mode+level to the pump's modbus."""
|
|
1113
|
+
url = "http://192.168.1.100/setRS485Live?BADU_ECO_DRIVE_II,1,hz,45"
|
|
1114
|
+
mock_aioresponse.get(url, body='"1|0,0|2,4500"', status=200)
|
|
1115
|
+
|
|
1116
|
+
result = await api_client.set_rs485_live(
|
|
1117
|
+
"BADU_ECO_DRIVE_II", slave_id=1, mode="hz", level=45
|
|
1118
|
+
)
|
|
1119
|
+
|
|
1120
|
+
assert "1|0,0|2,4500" in result
|
|
1121
|
+
|
|
1122
|
+
|
|
1123
|
+
@pytest.mark.asyncio
|
|
1124
|
+
async def test_set_rs485_live_rejects_bad_mode(
|
|
1125
|
+
api_client: VioletPoolAPI,
|
|
1126
|
+
) -> None:
|
|
1127
|
+
with pytest.raises(VioletPoolAPIError, match="Invalid RS485 mode"):
|
|
1128
|
+
await api_client.set_rs485_live(
|
|
1129
|
+
"BADU_ECO_DRIVE_II", slave_id=1, mode="percent", level=50
|
|
1130
|
+
)
|
|
1131
|
+
|
|
1132
|
+
|
|
1133
|
+
@pytest.mark.asyncio
|
|
1134
|
+
async def test_set_rs485_live_rejects_bad_slave_id(
|
|
1135
|
+
api_client: VioletPoolAPI,
|
|
1136
|
+
) -> None:
|
|
1137
|
+
with pytest.raises(ValueError, match="slave_id must be 1-247"):
|
|
1138
|
+
await api_client.set_rs485_live(
|
|
1139
|
+
"BADU_ECO_DRIVE_II", slave_id=0, mode="hz", level=45
|
|
1140
|
+
)
|
|
1141
|
+
|
|
1142
|
+
|
|
1143
|
+
@pytest.mark.asyncio
|
|
1144
|
+
async def test_end_rs485_live(
|
|
1145
|
+
mock_aioresponse: aioresponses,
|
|
1146
|
+
api_client: VioletPoolAPI,
|
|
1147
|
+
) -> None:
|
|
1148
|
+
"""Test end_rs485_live sends the DONE sentinel."""
|
|
1149
|
+
url = "http://192.168.1.100/setRS485Live?DONE"
|
|
1150
|
+
mock_aioresponse.get(url, body='"DONE"', status=200)
|
|
1151
|
+
|
|
1152
|
+
result = await api_client.end_rs485_live()
|
|
1153
|
+
|
|
1154
|
+
assert result == "DONE"
|
|
1155
|
+
|
|
1156
|
+
|
|
1157
|
+
@pytest.mark.asyncio
|
|
1158
|
+
async def test_get_live_trace(
|
|
1159
|
+
mock_aioresponse: aioresponses,
|
|
1160
|
+
api_client: VioletPoolAPI,
|
|
1161
|
+
) -> None:
|
|
1162
|
+
"""Test get_live_trace parses the 3-line CSV into a dict."""
|
|
1163
|
+
url = "http://192.168.1.100/getLiveTrace"
|
|
1164
|
+
csv_body = (
|
|
1165
|
+
"epoch;date;time;onewire1_value;pH_value;PUMP\n"
|
|
1166
|
+
"ms;;;°C;;\n"
|
|
1167
|
+
"1709234445000;29.02.2024;19:50:02;7,30;7,3;1\n"
|
|
1168
|
+
)
|
|
1169
|
+
mock_aioresponse.get(url, body=csv_body, status=200)
|
|
1170
|
+
|
|
1171
|
+
result = await api_client.get_live_trace()
|
|
1172
|
+
|
|
1173
|
+
assert result["onewire1_value"] == "7.30" # German comma → dot
|
|
1174
|
+
assert result["pH_value"] == "7.3"
|
|
1175
|
+
assert result["PUMP"] == "1"
|
|
1176
|
+
assert result["date"] == "29.02.2024"
|
|
1177
|
+
|
|
1178
|
+
|
|
1179
|
+
@pytest.mark.asyncio
|
|
1180
|
+
async def test_get_live_trace_malformed(
|
|
1181
|
+
mock_aioresponse: aioresponses,
|
|
1182
|
+
api_client: VioletPoolAPI,
|
|
1183
|
+
) -> None:
|
|
1184
|
+
"""Malformed payloads (fewer than 3 lines) raise VioletPoolAPIError."""
|
|
1185
|
+
url = "http://192.168.1.100/getLiveTrace"
|
|
1186
|
+
mock_aioresponse.get(url, body="only_one_line", status=200)
|
|
1187
|
+
|
|
1188
|
+
with pytest.raises(VioletPoolAPIError, match="Malformed getLiveTrace"):
|
|
1189
|
+
await api_client.get_live_trace()
|
|
1190
|
+
|
|
1191
|
+
|
|
872
1192
|
@pytest.mark.asyncio
|
|
873
1193
|
async def test_control_pump(
|
|
874
1194
|
mock_aioresponse: aioresponses,
|
|
@@ -984,6 +1304,7 @@ def test_error_codes_structure() -> None:
|
|
|
984
1304
|
ERROR_SEVERITY_ALARM,
|
|
985
1305
|
ERROR_SEVERITY_WARNING,
|
|
986
1306
|
ERROR_SEVERITY_INFO,
|
|
1307
|
+
ERROR_SEVERITY_REMINDER,
|
|
987
1308
|
), f"Code {code} has invalid severity: {info['severity']}"
|
|
988
1309
|
|
|
989
1310
|
|
|
@@ -1012,6 +1333,35 @@ def test_error_codes_hardware_modules() -> None:
|
|
|
1012
1333
|
assert ERROR_CODES["0209"]["severity"] == ERROR_SEVERITY_ALARM
|
|
1013
1334
|
|
|
1014
1335
|
|
|
1336
|
+
def test_error_codes_omnitronic_faults_added() -> None:
|
|
1337
|
+
"""OmniTronic multi-port valve faults (0045/46/47/49) are present as ALARM."""
|
|
1338
|
+
# Regression test for missing backwash-valve codes – see CLAUDE.md notes.
|
|
1339
|
+
for code in ("0045", "0046", "0047", "0049"):
|
|
1340
|
+
assert code in ERROR_CODES, f"Missing OmniTronic code {code}"
|
|
1341
|
+
assert ERROR_CODES[code]["severity"] == ERROR_SEVERITY_ALARM
|
|
1342
|
+
|
|
1343
|
+
|
|
1344
|
+
def test_error_codes_h2o2_dosing_added() -> None:
|
|
1345
|
+
"""H2O2 dosing warnings (0142-0145) are present."""
|
|
1346
|
+
for code in ("0142", "0143", "0144", "0145"):
|
|
1347
|
+
assert code in ERROR_CODES, f"Missing H2O2 code {code}"
|
|
1348
|
+
assert ERROR_CODES[code]["severity"] == ERROR_SEVERITY_WARNING
|
|
1349
|
+
|
|
1350
|
+
|
|
1351
|
+
def test_error_codes_reminder_category() -> None:
|
|
1352
|
+
"""REMINDER-category codes are classified correctly (not INFO)."""
|
|
1353
|
+
# 0003 birthday, 0005 system status, 0010-0012 updates, 0180-0182 calibration.
|
|
1354
|
+
for code in ("0003", "0005", "0010", "0011", "0012", "0180", "0181", "0182"):
|
|
1355
|
+
assert ERROR_CODES[code]["severity"] == ERROR_SEVERITY_REMINDER, code
|
|
1356
|
+
|
|
1357
|
+
|
|
1358
|
+
def test_error_code_0005_text_corrected() -> None:
|
|
1359
|
+
"""Code 0005 is the generic system-status reminder, not cloud maintenance."""
|
|
1360
|
+
# Regression: previously held wrong text "Wartungsarbeiten am Cloud-Server".
|
|
1361
|
+
assert "Cloud-Server" not in ERROR_CODES["0005"]["message"]
|
|
1362
|
+
assert ERROR_CODES["0005"]["message"] == "Systemnachricht"
|
|
1363
|
+
|
|
1364
|
+
|
|
1015
1365
|
def test_error_codes_four_digit_format() -> None:
|
|
1016
1366
|
"""Test all error code keys are 4-digit zero-padded strings."""
|
|
1017
1367
|
for code in ERROR_CODES:
|
{violet_poolcontroller_api-0.0.29 → violet_poolcontroller_api-0.0.31}/tests/test_mock_server.py
RENAMED
|
@@ -77,18 +77,26 @@ async def _request(url: str, auth: aiohttp.BasicAuth | None = None) -> tuple[int
|
|
|
77
77
|
|
|
78
78
|
async def test_raw_auth() -> None:
|
|
79
79
|
print("=" * 60)
|
|
80
|
-
print("TEST 1: Raw HTTP -
|
|
80
|
+
print("TEST 1: Raw HTTP - /getReadings without credentials -> 200")
|
|
81
81
|
print("=" * 60)
|
|
82
82
|
status, body = await _request(f"http://{HOST}:{PORT}/getReadings?ALL")
|
|
83
|
+
assert status == 200, f"Expected 200, got {status}"
|
|
84
|
+
print(f" OK: status={status} body_len={len(body)}")
|
|
85
|
+
|
|
86
|
+
print()
|
|
87
|
+
print("=" * 60)
|
|
88
|
+
print("TEST 2: Raw HTTP - /getConfig without credentials -> 401")
|
|
89
|
+
print("=" * 60)
|
|
90
|
+
status, body = await _request(f"http://{HOST}:{PORT}/getConfig")
|
|
83
91
|
assert status == 401, f"Expected 401, got {status}"
|
|
84
92
|
print(f" OK: status={status} body={body!r}")
|
|
85
93
|
|
|
86
94
|
print()
|
|
87
95
|
print("=" * 60)
|
|
88
|
-
print("TEST
|
|
96
|
+
print("TEST 3: Raw HTTP - /getConfig with wrong password -> 401")
|
|
89
97
|
print("=" * 60)
|
|
90
98
|
status, body = await _request(
|
|
91
|
-
f"http://{HOST}:{PORT}/
|
|
99
|
+
f"http://{HOST}:{PORT}/getConfig",
|
|
92
100
|
auth=aiohttp.BasicAuth("admin", "wrongpassword"),
|
|
93
101
|
)
|
|
94
102
|
assert status == 401, f"Expected 401, got {status}"
|
|
@@ -96,10 +104,10 @@ async def test_raw_auth() -> None:
|
|
|
96
104
|
|
|
97
105
|
print()
|
|
98
106
|
print("=" * 60)
|
|
99
|
-
print("TEST
|
|
107
|
+
print("TEST 4: Raw HTTP - /getConfig with correct credentials -> 200")
|
|
100
108
|
print("=" * 60)
|
|
101
109
|
status, body = await _request(
|
|
102
|
-
f"http://{HOST}:{PORT}/
|
|
110
|
+
f"http://{HOST}:{PORT}/getConfig",
|
|
103
111
|
auth=aiohttp.BasicAuth(USER, PASS),
|
|
104
112
|
)
|
|
105
113
|
assert status == 200, f"Expected 200, got {status}"
|
|
@@ -107,7 +115,7 @@ async def test_raw_auth() -> None:
|
|
|
107
115
|
|
|
108
116
|
print()
|
|
109
117
|
print("=" * 60)
|
|
110
|
-
print("TEST
|
|
118
|
+
print("TEST 5: /mock/* endpoints bypass auth")
|
|
111
119
|
print("=" * 60)
|
|
112
120
|
status, body = await _request(f"http://{HOST}:{PORT}/mock/state")
|
|
113
121
|
assert status == 200, f"Expected 200, got {status}"
|
|
@@ -117,7 +125,7 @@ async def test_raw_auth() -> None:
|
|
|
117
125
|
async def test_api_client() -> None:
|
|
118
126
|
print()
|
|
119
127
|
print("=" * 60)
|
|
120
|
-
print("TEST
|
|
128
|
+
print("TEST 6: VioletPoolAPI - wrong credentials -> error on /getConfig")
|
|
121
129
|
print("=" * 60)
|
|
122
130
|
async with aiohttp.ClientSession() as session:
|
|
123
131
|
api_bad = VioletPoolAPI(
|
|
@@ -128,14 +136,14 @@ async def test_api_client() -> None:
|
|
|
128
136
|
max_retries=1,
|
|
129
137
|
)
|
|
130
138
|
try:
|
|
131
|
-
await api_bad.
|
|
139
|
+
await api_bad.get_config(["DOSAGE_phminus_setpoint"])
|
|
132
140
|
assert False, "Should have raised VioletPoolAPIError"
|
|
133
141
|
except VioletPoolAPIError as exc:
|
|
134
142
|
print(f" OK: VioletPoolAPIError raised: {exc}")
|
|
135
143
|
|
|
136
144
|
print()
|
|
137
145
|
print("=" * 60)
|
|
138
|
-
print("TEST
|
|
146
|
+
print("TEST 7: VioletPoolAPI - correct credentials -> full workflow")
|
|
139
147
|
print("=" * 60)
|
|
140
148
|
async with aiohttp.ClientSession() as session:
|
|
141
149
|
api = VioletPoolAPI(
|
|
@@ -229,7 +237,7 @@ async def test_api_client() -> None:
|
|
|
229
237
|
|
|
230
238
|
print()
|
|
231
239
|
print("=" * 60)
|
|
232
|
-
print("TEST
|
|
240
|
+
print("TEST 8: Error simulation with auth")
|
|
233
241
|
print("=" * 60)
|
|
234
242
|
async with aiohttp.ClientSession() as session:
|
|
235
243
|
async with session.get(f"http://{HOST}:{PORT}/mock/error?code=500&count=1") as r:
|
|
@@ -42,6 +42,7 @@ from .const_api import ( # noqa: F401
|
|
|
42
42
|
ERROR_CODES,
|
|
43
43
|
ERROR_SEVERITY_ALARM,
|
|
44
44
|
ERROR_SEVERITY_INFO,
|
|
45
|
+
ERROR_SEVERITY_REMINDER,
|
|
45
46
|
ERROR_SEVERITY_WARNING,
|
|
46
47
|
)
|
|
47
48
|
from .const_devices import ( # noqa: F401
|
|
@@ -131,5 +132,6 @@ __all__ = [
|
|
|
131
132
|
"ERROR_CODES",
|
|
132
133
|
"ERROR_SEVERITY_ALARM",
|
|
133
134
|
"ERROR_SEVERITY_INFO",
|
|
135
|
+
"ERROR_SEVERITY_REMINDER",
|
|
134
136
|
"ERROR_SEVERITY_WARNING",
|
|
135
137
|
]
|