Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Speedy discovery #126

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"aiohttp>=3.5.4, <4",
"async_timeout>=4.0.2",
"voluptuous>=0.11.5",
"mypy>=1.4.1",
],
setup_requires=[
"setuptools_scm",
Expand Down
18 changes: 6 additions & 12 deletions solax/discovery.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import asyncio
import logging
import typing
from solax.http_client import all_variations

from solax.inverter import Inverter, InverterError
from solax.inverters import (
QVOLTHYBG33P,
Expand Down Expand Up @@ -34,7 +30,6 @@
]



class DiscoveryError(Exception):
"""Raised when unable to discover inverter"""

Expand All @@ -58,14 +53,13 @@ async def discover(host, port, pwd="") -> Inverter:
inverter = inverter_class(client)
if inverter.identify(response):
return inverter
else:
failures.append(
(
client_name,
inverter_class.__name__,
"did not identify",
)
failures.append(
(
client_name,
inverter_class.__name__,
"did not identify",
)
)
except InverterError as ex:
failures.append(
(
Expand Down
4 changes: 2 additions & 2 deletions solax/http_client.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from enum import Enum

import aiohttp
from solax.inverter_error import InverterError

from solax.inverter_error import InverterError
from solax.utils import to_url


Expand Down Expand Up @@ -102,4 +102,4 @@ def all_variations(host, port, pwd=""):
"post": post,
"post_query": post_query,
"post_data": post_data,
}
}
78 changes: 36 additions & 42 deletions solax/inverter.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@

from solax.http_client import HttpClient
from solax.inverter_error import InverterError
from solax.units import Measurement, Total, Units
from solax.units import SensorUnit, Units
from solax.units import Measurement, SensorUnit, Total, Units

Transformer = Callable[[VarArg(float)], float]

Expand Down Expand Up @@ -67,63 +66,60 @@ class InverterDefinition:
class Inverter:
"""Base wrapper around Inverter HTTP API"""

@staticmethod
def common_response_schema() -> Callable[[Any], InverterRawResponse]:
return vol.Schema(
{
vol.Required("type"): vol.Any(str, int),
vol.Required(vol.Any("SN", "sn")): str,
vol.Required(vol.Any("ver", "version")): str,
vol.Required("Data"): vol.Schema(
vol.All(
[vol.Coerce(float)],
)
),
vol.Optional("Information"): list,
},
extra=vol.REMOVE_EXTRA,
)
# @staticmethod
# def common_response_schema() -> Callable[[Any], InverterRawResponse]:
# return vol.Schema(
# {
# vol.Required("type"): vol.Any(str, int),
# vol.Required(vol.Any("SN", "sn")): str,
# vol.Required(vol.Any("ver", "version")): str,
# vol.Required("Data"): vol.Schema(
# vol.All(
# [vol.Coerce(float)],
# )
# ),
# vol.Optional("Information"): list,
# },
# extra=vol.REMOVE_EXTRA,
# )

@classmethod
def response_decoder(cls) -> ResponseDecoder:
"""
Inverter implementations should override
this to return a decoding map
"""
raise NotImplementedError()

@classmethod
def inverter_identification(cls) -> InverterIdentification:
return InverterIdentification(99999)
raise NotImplementedError()


@classmethod
def inverter_definition(cls) -> InverterDefinition:
old_mapping = cls.response_decoder()
mapping: Dict[str, InverterDataValue] = {}
for k, v in old_mapping.items():
indexes = v[0]
for key, val in old_mapping.items():
indexes = val[0]
if isinstance(indexes, (tuple, list)):
indexes = tuple(indexes)
elif isinstance(indexes, int):
indexes = (indexes,)
else:
raise TypeError('unexpected index type')
unit = v[1]
raise TypeError("unexpected index type")

unit = val[1]
if isinstance(unit, Units):
unit = Measurement(unit)
if len(v) < 3:
mapping[k] = InverterDataValue(indexes, unit)

if len(val) < 3:
mapping[key] = InverterDataValue(indexes, unit)
continue
transformers: Union[Transformer, Tuple[Transformer, ...]] = v[2]# type: ignore
transformers: Union[Transformer, Tuple[Transformer, ...]] = val[2] # type: ignore
if not isinstance(transformers, tuple):
transformers = (transformers,)
mapping[k] = InverterDataValue(indexes, unit, transformers)
mapping[key] = InverterDataValue(indexes, unit, transformers)
return InverterDefinition(cls.inverter_identification(), mapping)


@staticmethod
def apply_transforms(
Expand All @@ -133,11 +129,8 @@ def apply_transforms(
transforms = mapping_instance.transformations
out = [data[i] for i in indexes]
for transform in transforms:
if isinstance(out, list):
out = [transform(*out)]
else:
out = [transform(out)]
return out[0] if isinstance(out, list) else out
out = [transform(*out)]
return out[0]

async def get_data(self) -> InverterResponse:
try:
Expand All @@ -161,9 +154,9 @@ def map_response_v2(
for k, mapping_instance in self.inverter_definition().mapping.items():
accumulator[k] = self.apply_transforms(data, mapping_instance)
return accumulator

_schema: vol.Schema = vol.Schema({})

@classmethod
def schema(cls) -> vol.Schema:
"""
Expand Down Expand Up @@ -202,13 +195,14 @@ def handle_response(self, resp: bytes) -> InverterResponse:

def identify(self, response: bytes) -> bool:
try:
inverter_response = self.handle_response(response)
_ = self.handle_response(response)
except (Invalid, MultipleInvalid) as ex:
_ = humanize_error(response, ex)
return False
return True

def __init__(self, http_client: HttpClient):
self.manufacturer = "Solax" # default value, override if necessary
self.http_client = http_client

def sensor_map(self) -> Dict[str, Tuple[int, Union[Measurement, Total]]]:
Expand All @@ -222,4 +216,4 @@ def sensor_map(self) -> Dict[str, Tuple[int, Union[Measurement, Total]]]:
idx = mapping.indexes[0]
sensors[name] = (idx, unit)

return sensors
return sensors
2 changes: 1 addition & 1 deletion solax/inverter_error.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
class InverterError(Exception):
"""Indicates error communicating with inverter"""
"""Indicates error communicating with inverter"""
34 changes: 11 additions & 23 deletions solax/inverters/qvolt_hyb_g3_3p.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import voluptuous as vol

from solax import utils
from solax.inverter import Inverter, ResponseDecoder
from solax.http_client import HttpClient
from solax.inverter import Inverter, ResponseDecoder
from solax.units import Total, Units
from solax.utils import div10, div100, to_signed, twoway_div10, twoway_div100, u16_packer
from solax.utils import (
div10,
div100,
to_signed,
twoway_div10,
twoway_div100,
u16_packer,
)


class QVOLTHYBG33P(Inverter):
Expand Down Expand Up @@ -45,9 +51,7 @@ def battery_modes(*arg: float):
3: "Feed-in Priority",
}.get(value, f"unmapped value '{value}'")

def __init__(
self, http_client: HttpClient
):
def __init__(self, http_client: HttpClient):
super().__init__(http_client)
self.manufacturer = "Qcells"

Expand Down Expand Up @@ -130,8 +134,7 @@ def response_decoder(cls) -> ResponseDecoder:
),
"Today's Battery Discharge Energy": (78, Units.KWH, div10),
"Today's Battery Charge Energy": (79, Units.KWH, div10),
"Total PV Energy": ((80, 81), Total(Units.KWH),
(u16_packer, div10)),
"Total PV Energy": ((80, 81), Total(Units.KWH), (u16_packer, div10)),
"Today's Energy": (82, Units.KWH, div10),
# 83-85: always 0
"Total Feed-in Energy": ((86, 87), Total(Units.KWH), (u16_packer, div100)),
Expand Down Expand Up @@ -163,18 +166,3 @@ def response_decoder(cls) -> ResponseDecoder:
# 169: div100 same as [39]
# 170-199: always 0
}

@classmethod
def _build(cls, host, port, pwd="", params_in_query=True):
url = utils.to_url(host, port)
http_client = HttpClient(url, Method.POST, pwd).with_default_data()

schema = cls._schema
response_decoder = cls.response_decoder()
response_parser = ResponseParser(schema, response_decoder)
return cls(http_client, response_parser)

@classmethod
def build_all_variants(cls, host, port, pwd=""):
versions = [cls._build(host, port, pwd)]
return versions
31 changes: 3 additions & 28 deletions solax/inverters/x1_boost.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import voluptuous as vol

from solax import utils
from solax.inverter import Inverter, HttpClient, InverterIdentification, ResponseDecoder
from solax.inverter import Inverter, InverterIdentification, ResponseDecoder
from solax.units import Total, Units
from solax.utils import div10, div100, max_float, to_signed
from solax.utils import div10, div100, to_signed


class X1Boost(Inverter):
Expand Down Expand Up @@ -55,29 +54,5 @@ def response_decoder(cls) -> ResponseDecoder:
"Inverter Temperature": (39, Units.C),
"Exported Power": (48, Units.W, to_signed),
"Total Export Energy": (50, Total(Units.KWH), div100),
"Total Import Energy": (50, Total(Units.KWH), div100),
"Total Import Energy": (52, Total(Units.KWH), div100),
}

@classmethod
def _build(cls, host, port, pwd="", params_in_query=True):
url = utils.to_url(host, port)
http_client = InverterHttpClient(url, Method.POST, pwd)
if params_in_query:
http_client.with_default_query()
else:
http_client.with_default_data()

headers = {"X-Forwarded-For": "5.8.8.8"}
http_client.with_headers(headers)
schema = cls._schema
response_decoder = cls.response_decoder()
response_parser = ResponseParser(schema, response_decoder)
return cls(http_client, response_parser)

@classmethod
def build_all_variants(cls, host, port, pwd=""):
versions = [
cls._build(host, port, pwd, True),
cls._build(host, port, pwd, False),
]
return versions
15 changes: 0 additions & 15 deletions solax/inverters/x1_hybrid_gen4.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import voluptuous as vol

from solax import utils
from solax.inverter import Inverter, InverterIdentification, ResponseDecoder
from solax.units import Total, Units
from solax.utils import div10, div100, to_signed, u16_packer
Expand All @@ -26,20 +25,6 @@ class X1HybridGen4(Inverter):
extra=vol.REMOVE_EXTRA,
)

@classmethod
def _build(cls, host, port, pwd="", params_in_query=True):
url = utils.to_url(host, port)
http_client = InverterHttpClient(url, Method.POST, pwd).with_default_data()

response_parser = ResponseParser(cls._schema, cls.response_decoder())
return cls(http_client, response_parser)

@classmethod
def build_all_variants(cls, host, port, pwd=""):
versions = [cls._build(host, port, pwd)]
return versions


@classmethod
def inverter_identification(cls) -> InverterIdentification:
return InverterIdentification(15)
Expand Down
3 changes: 1 addition & 2 deletions solax/inverters/x1_mini.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ class X1Mini(Inverter):
},
extra=vol.REMOVE_EXTRA,
)



@classmethod
def inverter_identification(cls) -> InverterIdentification:
return InverterIdentification(4, "X1-Boost-Air-Mini")
Expand Down
5 changes: 2 additions & 3 deletions solax/inverters/x1_mini_v34.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,10 @@ class X1MiniV34(Inverter):
},
extra=vol.REMOVE_EXTRA,
)



@classmethod
def inverter_identification(cls) -> InverterIdentification:
return InverterIdentification(999)#4)
return InverterIdentification(999) # 4)

@classmethod
def response_decoder(cls) -> ResponseDecoder:
Expand Down
Loading