Skip to content

Commit

Permalink
raise validation error for all zero response data (#68)
Browse files Browse the repository at this point in the history
  • Loading branch information
VadimKraus committed Mar 3, 2024
1 parent ce0732f commit 2c20404
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 8 deletions.
30 changes: 24 additions & 6 deletions solax/response_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,30 @@
from voluptuous.humanize import humanize_error

from solax.units import SensorUnit
from solax.utils import PackerBuilderResult
from solax.utils import PackerBuilderResult, contains_none_zero_value

_LOGGER = logging.getLogger(__name__)
_LOGGER.setLevel(logging.INFO)

InverterResponse = namedtuple("InverterResponse", "data, serial_number, version, type")

_KEY_DATA = "Data"
_KEY_SERIAL = "SN"
_KEY_VERSION = "version"
_KEY_VER = "ver"
_KEY_TYPE = "type"


GenericResponseSchema = vol.Schema(
{
vol.Required(_KEY_DATA): vol.Schema(contains_none_zero_value),
vol.Required(vol.Or(_KEY_SERIAL, _KEY_SERIAL.lower())): vol.All(),
vol.Required(vol.Or(_KEY_VERSION, _KEY_VER)): vol.All(),
vol.Required(_KEY_TYPE): vol.All(),
},
extra=vol.REMOVE_EXTRA,
)

SensorIndexSpec = Union[int, PackerBuilderResult]
ResponseDecoder = Dict[
str,
Expand All @@ -27,7 +44,8 @@

class ResponseParser:
def __init__(self, schema: vol.Schema, decoder: ResponseDecoder):
self.schema = schema
self.schema = vol.And(schema, GenericResponseSchema)

self.response_decoder = decoder

def _decode_map(self) -> Dict[str, SensorIndexSpec]:
Expand Down Expand Up @@ -82,8 +100,8 @@ def handle_response(self, resp: bytearray):
_ = humanize_error(json_response, ex)
raise
return InverterResponse(
data=self.map_response(response["Data"]),
serial_number=response.get("SN", response.get("sn")),
version=response.get("ver", response.get("version")),
type=response["type"],
data=self.map_response(response[_KEY_DATA]),
serial_number=response.get(_KEY_SERIAL, response.get(_KEY_SERIAL.lower())),
version=response.get(_KEY_VER, response.get(_KEY_VERSION)),
type=response[_KEY_TYPE],
)
17 changes: 16 additions & 1 deletion solax/utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from typing import Protocol, Tuple
from numbers import Number
from typing import List, Protocol, Tuple

from voluptuous import Invalid

Expand Down Expand Up @@ -88,3 +89,17 @@ def twoway_div100(val):

def to_url(host, port):
return f"http://{host}:{port}/"


def contains_none_zero_value(value: List[Number]):
"""Validate that at least one element is not zero.
Args:
value (List[Number]): list to validate
Raises:
Invalid: if all elements are zero
"""

if isinstance(value, list):
if len(value) != 0 and any((v != 0 for v in value)):
return value
raise Invalid("All elements in the list {actual} are zero")
1 change: 1 addition & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# pylint: disable=unused-import
from tests.fixtures import inverters_fixture # noqa: F401
from tests.fixtures import inverters_fixture_all_zero # noqa: F401
from tests.fixtures import inverters_garbage_fixture # noqa: F401
from tests.fixtures import inverters_under_test # noqa: F401
from tests.fixtures import simple_http_fixture # noqa: F401
25 changes: 25 additions & 0 deletions tests/fixtures.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from collections import namedtuple
from copy import copy

import pytest

Expand Down Expand Up @@ -294,3 +295,27 @@ def inverters_garbage_fixture(httpserver, request):
query_string=request.param.query_string,
).respond_with_json({"bingo": "bango"})
yield ((httpserver.host, httpserver.port), request.param.inverter)


@pytest.fixture(params=INVERTERS_UNDER_TEST)
def inverters_fixture_all_zero(httpserver, request):
"""Use defined responses but replace the data with all zero values.
Testing incorrect responses.
"""

response = request.param.response
response = copy(response)
response["Data"] = [0] * (len(response["Data"]))

httpserver.expect_request(
uri=request.param.uri,
method=request.param.method,
query_string=request.param.query_string,
headers=request.param.headers,
data=request.param.data,
).respond_with_json(response)
yield (
(httpserver.host, httpserver.port),
request.param.inverter,
request.param.values,
)
15 changes: 15 additions & 0 deletions tests/test_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,18 @@ def test_inverter_sensors_define_valid_units(inverters_under_test):
f"is not a proper Unit on sensor '{name}' of Inverter '{inverters_under_test}'"
)
assert isinstance(unit, Measurement), msg


@pytest.mark.asyncio
async def test_smoke_zero(inverters_fixture_all_zero):
"""Responses with all zero values should be treated as an error.
Args:
inverters_fixture_all_zero (_type_): all responses with zero value data
"""
conn, inverter_class, _ = inverters_fixture_all_zero

# msg = 'all zero values should be discarded'
with pytest.raises(InverterError):
inv = await build_right_variant(inverter_class, conn)
rt_api = solax.RealTimeAPI(inv)
await rt_api.get_data()
22 changes: 21 additions & 1 deletion tests/test_vol.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import pytest
from voluptuous import Invalid

from solax.utils import startswith
from solax.utils import contains_none_zero_value, startswith


def test_does_start_with():
Expand All @@ -23,3 +23,23 @@ def test_is_not_str():
actual = 1
with pytest.raises(Invalid):
startswith(expected)(actual)


def test_contains_none_zero_value():
with pytest.raises(Invalid):
contains_none_zero_value([0])

with pytest.raises(Invalid):
contains_none_zero_value([0, 0])

not_a_list = 1
with pytest.raises(Invalid):
contains_none_zero_value(not_a_list)

expected = [1, 0]
actual = contains_none_zero_value(expected)
assert actual == expected

expected = [-1, 1]
actual = contains_none_zero_value(expected)
assert actual == expected

0 comments on commit 2c20404

Please sign in to comment.