Skip to content

Commit c167cca

Browse files
Marcel43367yaleman
andauthored
Remove maxval validator when c1 flag set (#370)
* check maxval for v3 only if c1 is not set * maint: adding tests, reworking validation a little to be clearer, adding docs --------- Co-authored-by: yaleman <james@terminaloutcomes.com>
1 parent c7f1c02 commit c167cca

File tree

5 files changed

+118
-25
lines changed

5 files changed

+118
-25
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ An example configuration
7777

7878
## Contributing / Testing
7979

80-
`ruff`, and `mypy` should all pass before submitting a PR.
80+
`ruff`, `pytest` and `mypy` should all pass before submitting a PR.
8181

8282
## License
8383

pvoutput/base.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from datetime import datetime, time
44
from math import floor
55
import re
6-
from typing import Any, AnyStr, Dict, Union
6+
from typing import Any, Dict, Union
77

88
from .exceptions import InvalidRegexpError, DonationRequired
99

@@ -57,7 +57,7 @@ def get_time_by_base(self) -> str:
5757
@classmethod
5858
def _validate_format(
5959
cls,
60-
format_string: AnyStr,
60+
format_string: str,
6161
key: str,
6262
value: Any,
6363
) -> None:
@@ -138,10 +138,17 @@ def validate_data(self, data: Dict[str, Any], apiset: Dict[str, Any]) -> bool:
138138
# check for donation-only keys
139139
if apiset[key].get("donation_required") and not self.donation_made:
140140
raise DonationRequired(f"key {key} requires an account which has donated")
141-
# check if you're outside max/min values
142-
if apiset[key].get("maxval") and data.get(key) > apiset[key].get("maxval"):
143-
raise ValueError(f"{key} cannot be higher than {apiset[key]['maxval']}, is {data[key]}")
144-
if apiset[key].get("minval") and data.get(key) < apiset[key].get("minval"):
141+
142+
# check max/min value constraints
143+
# Special case: When c1 flag is set, v3 represents cumulative lifetime energy values
144+
# which can exceed the normal maximum validation limit of 200000 Wh
145+
should_skip_maxval_check = key == "v3" and data.get("c1") is not None
146+
147+
if not should_skip_maxval_check:
148+
if apiset[key].get("maxval") is not None and data.get(key) > apiset[key].get("maxval"):
149+
raise ValueError(f"{key} cannot be higher than {apiset[key]['maxval']}, is {data[key]}")
150+
151+
if apiset[key].get("minval") is not None and data.get(key) < apiset[key].get("minval"):
145152
raise ValueError(f"{key} cannot be lower than {apiset[key]['minval']}, is {data[key]}")
146153

147154
return True

pvoutput/parameters.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@
107107
"required": False,
108108
"description": "Energy Consumption (Wh)",
109109
"donation_required": False,
110-
"maxval": 200000,
110+
"maxval": 200000, # Note: maxval validation is bypassed when c1 flag is set for cumulative values
111111
},
112112
"v4": {
113113
"required": False,
@@ -178,11 +178,18 @@
178178
"required_oneof": {"keys": ["v1", "v2", "v3", "v4"]},
179179
}
180180
"""
181-
Cumulative Energy
182-
The following values are valid for the c1 flag.
181+
Cumulative Energy Flag (c1)
182+
183+
The c1 flag indicates when energy values represent cumulative lifetime totals rather than
184+
daily incremental values. The following values are valid for the c1 flag:
185+
183186
1 Both v1 and v3 values are lifetime energy values. Consumption and generation energy is reset to 0 at the start of the day.
184187
2 Only v1 generation is a lifetime energy value.
185188
3 Only v3 consumption is a lifetime energy value.
189+
190+
Important: When c1 is set (any value 1-3), the normal maximum validation limit for v3
191+
(200,000 Wh) is bypassed, as cumulative lifetime energy values can exceed this threshold.
192+
This is handled automatically in the validation logic in base.py.
186193
"""
187194

188195
ADDOUTPUT_PARAMETERS = {

pyproject.toml

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,7 @@ classifiers = [
1111
]
1212
license = { text = "MIT" }
1313
requires-python = ">=3.10"
14-
dependencies = [
15-
"requests>=2.27.1",
16-
"aiohttp>=3.8.1",
17-
]
14+
dependencies = ["requests>=2.27.1", "aiohttp>=3.8.1"]
1815
version = "0.1.0"
1916
description = "Interface to the PVOutput API"
2017
readme = "README.md"
@@ -23,7 +20,6 @@ readme = "README.md"
2320
homepage = "https://yaleman.github.io/pvoutput/"
2421
repository = "https://github.com/yaleman/pvoutput/"
2522

26-
2723
[build-system]
2824
requires = ["pdm-backend"]
2925
build-backend = "pdm.backend"

tests/test_parameters.py

Lines changed: 93 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,12 @@
11
"""tests parameters things"""
22

3-
# import pytest
4-
# import pvoutput
5-
6-
7-
# def test_parameters_copying():
8-
# """checks if the copied values are different"""
9-
# assert pvoutput.ADDSTATUS_PARAMETERS["t"] != pvoutput.DELETESTATUS_PARAMETERS["t"]
10-
3+
import copy
114
from datetime import datetime
12-
13-
from pvoutput.parameters import ADDSTATUS_PARAMETERS
5+
from typing import Any, Dict
6+
import pytest
147
from pvoutput import PVOutput
8+
from pvoutput.base import PVOutputBase
9+
from pvoutput.parameters import ADDSTATUS_PARAMETERS
1510

1611

1712
def test_addstatus_default_date() -> None:
@@ -32,3 +27,91 @@ def testit(test: PVOutput) -> None:
3227
assert test_data["d"] == "20370111"
3328

3429
testit(test)
30+
31+
def test_parameters_copying() -> None:
32+
"""checks if the copied values are different"""
33+
# This test is commented out but kept for reference
34+
# assert pvoutput.ADDSTATUS_PARAMETERS["t"] != pvoutput.DELETESTATUS_PARAMETERS["t"]
35+
pass
36+
37+
38+
class TestC1V3Validation:
39+
"""Test cases for c1 flag and v3 validation logic"""
40+
41+
def setup_method(self) -> None:
42+
"""Setup test fixtures"""
43+
self.pvo_base = PVOutputBase(apikey="test", systemid=123)
44+
45+
def test_v3_exceeds_maxval_without_c1_flag_should_fail(self) -> None:
46+
"""Test that v3 values > 200000 fail validation when c1 is not set"""
47+
data = {"v3": 250000} # Exceeds maxval of 200000
48+
49+
with pytest.raises(ValueError, match="v3 cannot be higher than 200000"):
50+
self.pvo_base.validate_data(data, ADDSTATUS_PARAMETERS)
51+
52+
def test_v3_exceeds_maxval_with_c1_flag_should_pass(self) -> None:
53+
"""Test that v3 values > 200000 pass validation when c1 is set"""
54+
data = {"v3": 250000, "c1": 1} # c1 flag set, should bypass maxval check
55+
56+
# Should not raise an exception
57+
result = self.pvo_base.validate_data(data, ADDSTATUS_PARAMETERS)
58+
assert result is True
59+
60+
def test_v3_exceeds_maxval_with_c1_flag_value_2_should_pass(self) -> None:
61+
"""Test that v3 values > 200000 pass validation when c1=2"""
62+
data = {"v3": 300000, "c1": 2} # c1=2, should bypass maxval check
63+
64+
result = self.pvo_base.validate_data(data, ADDSTATUS_PARAMETERS)
65+
assert result is True
66+
67+
def test_v3_exceeds_maxval_with_c1_flag_value_3_should_pass(self) -> None:
68+
"""Test that v3 values > 200000 pass validation when c1=3"""
69+
data = {"v3": 500000, "c1": 3} # c1=3, should bypass maxval check
70+
71+
result = self.pvo_base.validate_data(data, ADDSTATUS_PARAMETERS)
72+
assert result is True
73+
74+
def test_v3_within_maxval_without_c1_flag_should_pass(self) -> None:
75+
"""Test that v3 values <= 200000 pass validation without c1 flag"""
76+
data = {"v3": 150000} # Within maxval limit
77+
78+
result = self.pvo_base.validate_data(data, ADDSTATUS_PARAMETERS)
79+
assert result is True
80+
81+
def test_v3_within_maxval_with_c1_flag_should_pass(self) -> None:
82+
"""Test that v3 values <= 200000 pass validation with c1 flag"""
83+
data = {"v3": 150000, "c1": 1} # Within limit, c1 set
84+
85+
result = self.pvo_base.validate_data(data, ADDSTATUS_PARAMETERS)
86+
assert result is True
87+
88+
def test_other_fields_maxval_validation_unaffected_by_c1(self) -> None:
89+
"""Test that c1 flag doesn't affect maxval validation of other fields"""
90+
# Create a modified parameter set with maxval for testing
91+
test_params: Dict[str, Any] = copy.deepcopy(ADDSTATUS_PARAMETERS)
92+
test_params["v2"] = {"maxval": 1000, "type": int}
93+
94+
data = {"v2": 1500, "c1": 1} # v2 exceeds maxval, c1 set
95+
96+
with pytest.raises(ValueError, match="v2 cannot be higher than 1000"):
97+
self.pvo_base.validate_data(data, test_params)
98+
99+
def test_v3_minval_validation_still_applies_with_c1(self) -> None:
100+
"""Test that minval validation for v3 still applies even when c1 is set"""
101+
# Create a deep copy of parameters and modify v3 to have minval
102+
test_params: Dict[str, Any] = copy.deepcopy(ADDSTATUS_PARAMETERS)
103+
test_params["v3"]["minval"] = 0
104+
105+
data = {"v3": -100, "c1": 1} # Negative value, c1 set
106+
107+
with pytest.raises(ValueError, match="v3 cannot be lower than 0"):
108+
self.pvo_base.validate_data(data, test_params)
109+
110+
def test_c1_flag_zero_does_not_bypass_v3_maxval(self) -> None:
111+
"""Test that c1=0 (if somehow passed) doesn't bypass v3 maxval validation"""
112+
data = {"v3": 250000, "c1": 0} # c1=0 is not a valid flag value
113+
114+
# This should fail because c1=0 is not considered "set" for our logic
115+
# Note: This would also fail format validation, but testing the maxval logic specifically
116+
with pytest.raises(ValueError):
117+
self.pvo_base.validate_data(data, ADDSTATUS_PARAMETERS)

0 commit comments

Comments
 (0)