Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master' into pullrequests/VadimK…
Browse files Browse the repository at this point in the history
…raus/feature/query_check

Signed-off-by: Robin Wohlers-Reichel <[email protected]>

# Conflicts:
#	solax/inverter.py
#	solax/utils.py
  • Loading branch information
squishykid committed Nov 27, 2022
2 parents a7fecf0 + 7feccc8 commit 7a38b02
Show file tree
Hide file tree
Showing 12 changed files with 531 additions and 173 deletions.
6 changes: 5 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@
license="MIT",
url="https://github.com/squishykid/solax",
packages=setuptools.find_packages(exclude=["tests", "tests.*"]),
install_requires=["aiohttp>=3.5.4, <4", "voluptuous>=0.11.5"],
install_requires=[
"aiohttp>=3.5.4, <4",
"async_timeout>=4.0.2",
"voluptuous>=0.11.5",
],
setup_requires=[
"setuptools_scm",
],
Expand Down
14 changes: 13 additions & 1 deletion solax/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,22 @@
X1Smart,
QVOLTHYBG33P,
X1Boost,
X1HybridGen4,
)

# registry of inverters
REGISTRY = [XHybrid, X3, X3V34, X1, X1Mini, X1MiniV34, X1Smart, QVOLTHYBG33P, X1Boost]
REGISTRY = [
XHybrid,
X3,
X3V34,
X1,
X1Mini,
X1MiniV34,
X1Smart,
QVOLTHYBG33P,
X1Boost,
X1HybridGen4,
]


class DiscoveryError(Exception):
Expand Down
86 changes: 63 additions & 23 deletions solax/inverter.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from solax.utils import contains_none_zero_value

from solax.units import Measurement, SensorUnit, Units
from solax.utils import PackerBuilderResult


class InverterError(Exception):
Expand All @@ -33,18 +34,21 @@ class InverterError(Exception):
extra=vol.REMOVE_EXTRA,
)

SensorIndexSpec = Union[int, PackerBuilderResult]
ResponseDecoder = Dict[
str,
Union[
Tuple[SensorIndexSpec, SensorUnit],
Tuple[SensorIndexSpec, SensorUnit, Callable[[Any], Any]],
],
]


class Inverter:
"""Base wrapper around Inverter HTTP API"""

ResponseDecoderType = Union[
Dict[str, int],
Dict[str, Tuple[int, SensorUnit]],
Dict[str, Tuple[int, SensorUnit, Callable[[Any, Any], Any]]],
]

@classmethod
def response_decoder(cls) -> ResponseDecoderType:
def response_decoder(cls) -> ResponseDecoder:
"""
Inverter implementations should override
this to return a decoding map
Expand Down Expand Up @@ -86,35 +90,43 @@ async def make_request(cls, host, port, pwd="", headers=None) -> InverterRespons
def sensor_map(cls) -> Dict[str, Tuple[int, Measurement]]:
"""
Return sensor map
Warning, HA depends on this
"""
sensors = {}
sensors: Dict[str, Tuple[int, Measurement]] = {}
for name, mapping in cls.response_decoder().items():
unit = Measurement(Units.NONE)

if isinstance(mapping, tuple):
(idx, unit_or_measurement, *_) = mapping
else:
idx = mapping
(idx, unit_or_measurement, *_) = mapping

if isinstance(unit_or_measurement, Units):
unit = Measurement(unit_or_measurement)
else:
unit = unit_or_measurement
if isinstance(idx, tuple):
sensor_indexes = idx[0]
first_sensor_index = sensor_indexes[0]
idx = first_sensor_index
sensors[name] = (idx, unit)
return sensors

@classmethod
def postprocess_map(cls) -> Dict[str, Callable[[Any, Any], Any]]:
def _decode_map(cls) -> Dict[str, SensorIndexSpec]:
sensors: Dict[str, SensorIndexSpec] = {}
for name, mapping in cls.response_decoder().items():
sensors[name] = mapping[0]
return sensors

@classmethod
def _postprocess_map(cls) -> Dict[str, Callable[[Any], Any]]:
"""
Return map of functions to be applied to each sensor value
"""
sensors = {}
sensors: Dict[str, Callable[[Any], Any]] = {}
for name, mapping in cls.response_decoder().items():
if isinstance(mapping, tuple):
processor = None
(_, _, *processor) = mapping
if processor:
sensors[name] = processor[0]
processor = None
(_, _, *processor) = mapping
if processor:
sensors[name] = processor[0]
return sensors

@classmethod
Expand All @@ -127,11 +139,17 @@ def schema(cls) -> vol.Schema:
@classmethod
def map_response(cls, resp_data) -> Dict[str, Any]:
result = {}
for sensor_name, (idx, _) in cls.sensor_map().items():
val = resp_data[idx]
for sensor_name, decode_info in cls._decode_map().items():
if isinstance(decode_info, (tuple, list)):
indexes = decode_info[0]
packer = decode_info[1]
values = tuple(resp_data[i] for i in indexes)
val = packer(*values)
else:
val = resp_data[decode_info]
result[sensor_name] = val
for sensor_name, processor in cls.postprocess_map().items():
result[sensor_name] = processor(result[sensor_name], result)
for sensor_name, processor in cls._postprocess_map().items():
result[sensor_name] = processor(result[sensor_name])
return result

@classmethod
Expand Down Expand Up @@ -178,6 +196,28 @@ async def make_request(cls, host, port=80, pwd="", headers=None):
url = base.format(host, port, pwd)
async with aiohttp.ClientSession() as session:
async with session.post(url, headers=headers) as req:
req.raise_for_status()
resp = await req.read()

return cls.handle_response(resp)


class InverterPostData(InverterPost):
# 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):
base = "http://{}:{}/"
url = base.format(host, port)
data = "optType=ReadRealTimeData"
if pwd:
data = data + "&pwd=" + pwd
async with aiohttp.ClientSession() as session:
async with session.post(
url, headers=headers, data=data.encode("utf-8")
) as req:
req.raise_for_status()
resp = await req.read()

return cls.handle_response(resp)
2 changes: 2 additions & 0 deletions solax/inverters/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from .qvolt_hyb_g3_3p import QVOLTHYBG33P
from .x_hybrid import XHybrid
from .x1 import X1
from .x1_hybrid_gen4 import X1HybridGen4
from .x1_mini import X1Mini
from .x1_mini_v34 import X1MiniV34
from .x1_smart import X1Smart
Expand All @@ -18,4 +19,5 @@
"X3V34",
"X3",
"X1Boost",
"X1HybridGen4",
]
37 changes: 17 additions & 20 deletions solax/inverters/qvolt_hyb_g3_3p.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,10 @@
from solax.utils import (
div10,
div100,
pack_u16,
twoway_div10,
to_signed,
pv_energy,
twoway_div100,
total_energy,
discharge_energy,
charge_energy,
feedin_energy,
consumption,
)


Expand All @@ -29,7 +24,7 @@ class Processors:
"""

@staticmethod
def inverter_modes(value, *_args, **_kwargs):
def inverter_modes(value):
return {
0: "Waiting",
1: "Checking",
Expand All @@ -45,7 +40,7 @@ def inverter_modes(value, *_args, **_kwargs):
}.get(value, f"unmapped value '{value}'")

@staticmethod
def battery_modes(value, *_args, **_kwargs):
def battery_modes(value):
return {
0: "Self Use Mode",
1: "Force Time Use",
Expand Down Expand Up @@ -121,24 +116,26 @@ def response_decoder(cls):
# 53: always 0
# 54: follows PV Output, idles around 35, peaks at 54,
# 55-67: always 0
"Total Energy": (68, Total(Units.KWH), total_energy),
"Total Energy Resets": (69),
"Total Energy": (pack_u16(68, 69), Total(Units.KWH), div10),
# 70: div10, today's energy including battery usage
# 71-73: 0
"Total Battery Discharge Energy": (74, Total(Units.KWH), discharge_energy),
"Total Battery Discharge Energy Resets": (75),
"Total Battery Charge Energy": (76, Total(Units.KWH), charge_energy),
"Total Battery Charge Energy Resets": (77),
"Total Battery Discharge Energy": (
pack_u16(74, 75),
Total(Units.KWH),
div10,
),
"Total Battery Charge Energy": (
pack_u16(76, 77),
Total(Units.KWH),
div10,
),
"Today's Battery Discharge Energy": (78, Units.KWH, div10),
"Today's Battery Charge Energy": (79, Units.KWH, div10),
"Total PV Energy": (80, Total(Units.KWH), pv_energy),
"Total PV Energy Resets": (81),
"Total PV Energy": (pack_u16(80, 81), Total(Units.KWH), div10),
"Today's Energy": (82, Units.KWH, div10),
# 83-85: always 0
"Total Feed-in Energy": (86, Total(Units.KWH), feedin_energy),
"Total Feed-in Energy Resets": (87),
"Total Consumption": (88, Total(Units.KWH), consumption),
"Total Consumption Resets": (89),
"Total Feed-in Energy": (pack_u16(86, 87), Total(Units.KWH), div100),
"Total Consumption": (pack_u16(88, 89), Total(Units.KWH), div100),
"Today's Feed-in Energy": (90, Units.KWH, div100),
# 91: always 0
"Today's Consumption": (92, Units.KWH, div100),
Expand Down
50 changes: 50 additions & 0 deletions solax/inverters/x1_hybrid_gen4.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import voluptuous as vol
from solax.inverter import InverterPostData
from solax.units import Units, Total
from solax.utils import div10, div100, pack_u16, to_signed


class X1HybridGen4(InverterPostData):
# pylint: disable=duplicate-code
_schema = vol.Schema(
{
vol.Required("type"): vol.All(int, 15),
vol.Required(
"sn",
): str,
vol.Required("ver"): str,
vol.Required("Data"): vol.Schema(
vol.All(
[vol.Coerce(float)],
vol.Length(min=200, max=200),
)
),
vol.Required("Information"): vol.Schema(vol.All(vol.Length(min=9, max=10))),
},
extra=vol.REMOVE_EXTRA,
)

@classmethod
def response_decoder(cls):
return {
"AC voltage R": (0, Units.V, div10),
"AC current": (1, Units.A, div10),
"AC power": (2, Units.W),
"Grid frequency": (3, Units.HZ, div100),
"PV1 voltage": (4, Units.V, div10),
"PV2 voltage": (5, Units.V, div10),
"PV1 current": (6, Units.A, div10),
"PV2 current": (7, Units.A, div10),
"PV1 power": (8, Units.W),
"PV2 power": (9, Units.W),
"On-grid total yield": (pack_u16(11, 12), Total(Units.KWH), div10),
"On-grid daily yield": (13, Units.KWH, div10),
"Battery voltage": (14, Units.V, div100),
"Battery current": (15, Units.A, div100),
"Battery power": (16, Units.W),
"Battery temperature": (17, Units.C),
"Battery SoC": (18, Units.PERCENT),
"Grid power": (32, Units.W, to_signed),
"Total feed-in energy": (pack_u16(34, 35), Total(Units.KWH), div100),
"Total consumption": (pack_u16(36, 37), Total(Units.KWH), div100),
}
37 changes: 16 additions & 21 deletions solax/inverters/x3_v34.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,10 @@
from solax.utils import (
div10,
div100,
pack_u16,
twoway_div10,
to_signed,
pv_energy,
twoway_div100,
total_energy,
discharge_energy,
charge_energy,
feedin_energy,
consumption,
eps_total_energy,
)


Expand Down Expand Up @@ -57,36 +51,37 @@ def response_decoder(cls):
"PV2 Current": (12, Units.A, div10),
"PV1 Power": (13, Units.W),
"PV2 Power": (14, Units.W),
"Total PV Energy": (89, Total(Units.KWH), pv_energy),
"Total PV Energy Resets": (90),
"Total PV Energy": (pack_u16(89, 90), Total(Units.KWH), div10),
"Today's PV Energy": (112, Units.KWH, div10),
"Grid Frequency Phase 1": (15, Units.HZ, div100),
"Grid Frequency Phase 2": (16, Units.HZ, div100),
"Grid Frequency Phase 3": (17, Units.HZ, div100),
"Total Energy": (19, Total(Units.KWH), total_energy),
"Total Energy Resets": (20),
"Total Energy": (pack_u16(19, 20), Total(Units.KWH), div10),
"Today's Energy": (21, Units.KWH, div10),
"Battery Voltage": (24, Units.V, div100),
"Battery Current": (25, Units.A, twoway_div100),
"Battery Power": (26, Units.W, to_signed),
"Battery Temperature": (27, Units.C),
"Battery Remaining Capacity": (28, Units.PERCENT),
"Total Battery Discharge Energy": (30, Total(Units.KWH), discharge_energy),
"Total Battery Discharge Energy Resets": (31),
"Total Battery Discharge Energy": (
pack_u16(30, 31),
Total(Units.KWH),
div10,
),
"Today's Battery Discharge Energy": (113, Units.KWH, div10),
"Battery Remaining Energy": (32, Units.KWH, div10),
"Total Battery Charge Energy": (87, Total(Units.KWH), charge_energy),
"Total Battery Charge Energy Resets": (88),
"Total Battery Charge Energy": (
pack_u16(87, 88),
Total(Units.KWH),
div10,
),
"Today's Battery Charge Energy": (114, Units.KWH, div10),
"Exported Power": (65, Units.W, to_signed),
"Total Feed-in Energy": (67, Total(Units.KWH), feedin_energy),
"Total Feed-in Energy Resets": (68),
"Total Consumption": (69, Total(Units.KWH), consumption),
"Total Consumption Resets": (70),
"Total Feed-in Energy": (pack_u16(67, 68), Total(Units.KWH), div100),
"Total Consumption": (pack_u16(69, 70), Total(Units.KWH), div100),
"AC Power": (181, Units.W, to_signed),
"EPS Frequency": (63, Units.HZ, div100),
"EPS Total Energy": (110, Units.KWH, eps_total_energy),
"EPS Total Energy Resets": (111, Units.HZ),
"EPS Total Energy": (pack_u16(110, 111), Units.KWH, div10),
}

# pylint: enable=duplicate-code
Loading

0 comments on commit 7a38b02

Please sign in to comment.