Skip to content

Commit

Permalink
raise validation error for all zero response data
Browse files Browse the repository at this point in the history
  • Loading branch information
VadimKraus committed Jul 31, 2022
1 parent 4c4bc83 commit 222d91d
Show file tree
Hide file tree
Showing 7 changed files with 128 additions and 36 deletions.
68 changes: 44 additions & 24 deletions solax/inverter.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import voluptuous as vol
from voluptuous import Invalid, MultipleInvalid
from voluptuous.humanize import humanize_error
from solax.utils import contains_none_zero_value

from solax.units import Measurement, SensorUnit, Units

Expand All @@ -15,6 +16,23 @@ class InverterError(Exception):

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

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


DataSchema = 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,
)


class Inverter:
"""Base wrapper around Inverter HTTP API"""
Expand Down Expand Up @@ -116,25 +134,6 @@ def map_response(cls, resp_data) -> Dict[str, Any]:
result[sensor_name] = processor(result[sensor_name], result)
return result


class InverterPost(Inverter):
# This is an intermediate abstract class,
# so we can disable the pylint warning
# pylint: disable=W0223,R0914
@classmethod
async def make_request(cls, host, port=80, pwd="", headers=None):
if not pwd:
base = "http://{}:{}/?optType=ReadRealTimeData"
url = base.format(host, port)
else:
base = "http://{}:{}/?optType=ReadRealTimeData&pwd={}&"
url = base.format(host, port, pwd)
async with aiohttp.ClientSession() as session:
async with session.post(url, headers=headers) as req:
resp = await req.read()

return cls.handle_response(resp)

@classmethod
def handle_response(cls, resp: bytearray):
"""
Expand All @@ -149,15 +148,36 @@ def handle_response(cls, resp: bytearray):

raw_json = resp.decode("utf-8")
json_response = json.loads(raw_json)
response = {}
response = {} # type: dict
try:
response = cls.schema()(json_response)
DataSchema(json_response)
except (Invalid, MultipleInvalid) as ex:
_ = humanize_error(json_response, ex)
raise

return InverterResponse(
data=cls.map_response(response["Data"]),
serial_number=response.get("SN", response.get("sn")),
version=response["ver"],
type=response["type"],
data=cls.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],
)


class InverterPost(Inverter):
# This is an intermediate abstract class,
# so we can disable the pylint warning
# pylint: disable=W0223,R0914
@classmethod
async def make_request(cls, host, port=80, pwd="", headers=None):
if not pwd:
base = "http://{}:{}/?optType=ReadRealTimeData"
url = base.format(host, port)
else:
base = "http://{}:{}/?optType=ReadRealTimeData&pwd={}&"
url = base.format(host, port, pwd)
async with aiohttp.ClientSession() as session:
async with session.post(url, headers=headers) as req:
resp = await req.read()

return cls.handle_response(resp)
13 changes: 3 additions & 10 deletions solax/inverters/x_hybrid.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import json
import aiohttp
import voluptuous as vol
from solax.inverter import Inverter, InverterResponse
from solax.inverter import Inverter
from solax.units import Total, Units


Expand Down Expand Up @@ -70,11 +69,5 @@ async def make_request(cls, host, port=80, pwd="", headers=None):
garbage = await req.read()
formatted = garbage.decode("utf-8")
formatted = formatted.replace(",,", ",0.0,").replace(",,", ",0.0,")
json_response = json.loads(formatted)
response = cls.schema()(json_response)
return InverterResponse(
data=cls.map_response(response["Data"]),
serial_number=response["SN"],
version=response["version"],
type=response["type"],
)

return cls.handle_response(formatted.encode("utf-8"))
18 changes: 18 additions & 0 deletions solax/utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from numbers import Number
from typing import List
from voluptuous import Invalid


Expand Down Expand Up @@ -85,3 +87,19 @@ def twoway_div10(val, *_args, **_kwargs):

def twoway_div100(val, *_args, **_kwargs):
return to_signed(val, None) / 100


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 next((v for v in value if v != 0), None) is not None:
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
Expand Up @@ -3,3 +3,4 @@
from tests.fixtures import inverters_garbage_fixture # noqa: F401
from tests.fixtures import inverters_fixture # noqa: F401
from tests.fixtures import inverters_under_test # noqa: F401
from tests.fixtures import inverters_fixture_all_zero # noqa: F401
24 changes: 24 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
import solax.inverters as inverter
from tests.samples.expected_values import (
Expand Down Expand Up @@ -193,6 +194,29 @@ def inverters_under_test(request):
yield 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,
).respond_with_json(response)
yield (
(httpserver.host, httpserver.port),
request.param.inverter,
request.param.values,
)


@pytest.fixture(params=INVERTERS_UNDER_TEST)
def inverters_fixture(httpserver, request):
httpserver.expect_request(
Expand Down
17 changes: 16 additions & 1 deletion tests/test_smoke.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
import pytest
import solax
from solax.inverter import InverterError
from solax.inverter import Inverter, InverterError
from solax.discovery import REGISTRY
from solax.units import Measurement
from tests import fixtures


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

# msg = 'all zero values should be discarded'
with pytest.raises(InverterError):
await inverter.get_data()


@pytest.mark.asyncio
async def test_smoke(inverters_fixture):
conn, inverter_class, values = inverters_fixture
Expand Down
23 changes: 22 additions & 1 deletion tests/test_vol.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
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 @@ -22,3 +22,24 @@ 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 222d91d

Please sign in to comment.