Skip to content

Commit

Permalink
Add support for X3 hybrid firmware version v2.034.06 (#37)
Browse files Browse the repository at this point in the history
  • Loading branch information
ppetru committed Aug 16, 2021
1 parent 19ed75a commit 6d93b10
Show file tree
Hide file tree
Showing 2 changed files with 257 additions and 35 deletions.
176 changes: 163 additions & 13 deletions solax/inverter.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@

import aiohttp
import voluptuous as vol
from voluptuous import Invalid
from voluptuous import Invalid, MultipleInvalid
from voluptuous.humanize import humanize_error


class InverterError(Exception):
Expand Down Expand Up @@ -57,20 +58,32 @@ def sensor_map(cls):
"""
raise NotImplementedError()

@classmethod
def postprocess_map(cls):
"""
Return map of functions to be applied to each sensor value
"""
return {}

@classmethod
def schema(cls):
"""
Return schema
"""
raise NotImplementedError()

@staticmethod
def map_response(resp_data, sensor_map):
return {
sensor_name: resp_data[i]
for sensor_name, (i, _)
in sensor_map.items()
}
@classmethod
def map_response(cls, resp_data):
result = {}
for sensor_name, (idx, _) in cls.sensor_map().items():
if idx < 0:
val = None
else:
val = resp_data[idx]
result[sensor_name] = val
for sensor_name, processor in cls.postprocess_map().items():
result[sensor_name] = processor(result[sensor_name], result)
return result


async def discover(host, port, pwd='') -> Inverter:
Expand Down Expand Up @@ -167,7 +180,7 @@ async def make_request(cls, host, port=80, pwd=''):
json_response = json.loads(formatted)
response = cls.schema()(json_response)
return InverterResponse(
data=cls.map_response(response['Data'], cls.__sensor_map),
data=cls.map_response(response['Data']),
serial_number=response['SN'],
version=response['version'],
type=response['type']
Expand Down Expand Up @@ -199,10 +212,20 @@ async def make_request(cls, host, port=80, pwd=''):
resp = await req.read()
raw_json = resp.decode("utf-8")
json_response = json.loads(raw_json)
response = cls.schema()(json_response)
response = {}
try:
response = cls.schema()(json_response)
except (Invalid, MultipleInvalid) as ex:
_ = humanize_error(json_response, ex)
# print(_)
raise
if 'SN' in response:
serial_number = response['SN']
else:
serial_number = response['sn']
return InverterResponse(
data=cls.map_response(response['Data'], cls.sensor_map()),
serial_number=response['SN'],
data=cls.map_response(response['Data']),
serial_number=serial_number,
version=response['ver'],
type=response['type']
)
Expand Down Expand Up @@ -287,6 +310,133 @@ def schema(cls):
return cls.__schema


def _energy(value, result):
value += result['Total Feed-in Energy Resets'] * 65535
value /= 100
return value


def _consumption(value, result):
value += result['Total Consumption Resets'] * 65535
value /= 100
return value


def _twoway_current(val, _):
return _to_signed(val, None) / 10


def _div10(val, _):
return val / 10


def _div100(val, _):
return val / 100


def _to_signed(val, _):
if val > 32767:
val -= 65535
return val


def _pv_power(_, result):
return result['PV1 Power'] + result['PV2 Power']


def _load_power(_, result):
return result['AC Power'] - result['Exported Power']


class X3V34(InverterPost):
"""X3 v2.034.06"""
__schema = vol.Schema({
vol.Required('type'): vol.All(int, 5),
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=10, max=10)
)
),
}, extra=vol.REMOVE_EXTRA)

__sensor_map = {
'Network Voltage Phase 1': (0, 'V', _div10),
'Network Voltage Phase 2': (1, 'V', _div10),
'Network Voltage Phase 3': (2, 'V', _div10),

'Output Current Phase 1': (3, 'A', _div10),
'Output Current Phase 2': (4, 'A', _div10),
'Output Current Phase 3': (5, 'A', _div10),

'Power Now Phase 1': (6, 'W'),
'Power Now Phase 2': (7, 'W'),
'Power Now Phase 3': (8, 'W'),

'PV1 Voltage': (9, 'V', _div10),
'PV2 Voltage': (10, 'V', _div10),
'PV1 Current': (11, 'A', _div10),
'PV2 Current': (12, 'A', _div10),
'PV1 Power': (13, 'W'),
'PV2 Power': (14, 'W'),
'Total PV Power': (-1, 'W', _pv_power),

'Grid Frequency Phase 1': (15, 'Hz', _div100),
'Grid Frequency Phase 2': (16, 'Hz', _div100),
'Grid Frequency Phase 3': (17, 'Hz', _div100),

'Total Energy': (19, 'kWh', _div10),
'Today\'s Energy': (21, 'kWh', _div10),

'Battery Voltage': (24, 'V', _div100),
'Battery Current': (25, 'A', _twoway_current),
'Battery Power': (26, 'W', _to_signed),
'Battery Temperature': (27, 'C'),
'Battery Remaining Capacity': (28, '%'),

'Exported Power': (65, 'W', _to_signed),
'Total Feed-in Energy': (67, 'kWh', _energy),
'Total Feed-in Energy Resets': (68, ''),
'Total Consumption': (69, 'kWh', _consumption),
'Total Consumption Resets': (70, ''),

'AC Power': (181, 'W'),
'Load Power': (-2, 'W', _load_power),
}

@classmethod
def sensor_map(cls):
"""
Return sensor map
"""
sensors = {}
for name, (idx, unit, *_) in cls.__sensor_map.items():
sensors[name] = (idx, unit)
return sensors

@classmethod
def postprocess_map(cls):
"""
Return postprocessing map
"""
sensors = {}
for name, (_, _, *processor) in cls.__sensor_map.items():
if processor:
sensors[name] = processor[0]
return sensors

@classmethod
def schema(cls):
return cls.__schema


class X1(InverterPost):
__schema = vol.Schema({
vol.Required('type'): vol.All(
Expand Down Expand Up @@ -411,4 +561,4 @@ def schema(cls):


# registry of inverters
REGISTRY = [XHybrid, X3, X1, X1Mini]
REGISTRY = [XHybrid, X3, X3V34, X1, X1Mini]
116 changes: 94 additions & 22 deletions tests/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,28 +129,6 @@
1, 3.00, 0.00, 3.17, 1.01]
}

X3_HYBRID_G3_2X_MPPT_RESPONSE = {
"type": "X3-Hybiyd-G3",
"SN": "XXXXXXXXXX",
"ver": "2.033.20",
"Data": [0.0, 0.0, 0.0, 0.0, 0.9, 234.0, 3189, 42, 15.2, 27.0, -25, 0, 0,
210.30, -15.70, -3321, 24, 8.6, 0, 11.0, 1, 68, 232.4, 170.0,
31.0, 35.0, 22.6, 20.7, 3.8, 3.8, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,
2.03, 23.41, 123, 1344, 1722, 5.4, 6.9, 250.0, 251.9, 50.01,
50.01, 50.01, 0.0, 0.0, 0, 0.00, 0, 0, 0, 0.00, 0, 0, 0, 0, 0.00,
0, 0, 2, 1, 26, 1.00, 0, 100, 10, 25.00, 25.00, 0, 0, 0, 0, 0.0,
10.8, 24.4, 0.0, 0, 2, 0.0, 0.0, 0, 0.0, 0.0, 0, 0, 0, 0, 1, 1,
0, 0, 0.00, 0.00, 1, 273, 212.3, -16.2, -3439],
"Information": [8.000, 5, "X3-Hybiyd-G3", "XXXXXXXX", 1, 4.47, 0.00, 4.34,
1.05],
"battery": {
"brand": "83",
"masterVer": "1.11",
"slaveNum": "4",
"slaveVer": [1.13, 1.13, 1.13, 1.13]
}
}

XHYBRID_VALUES = {
'Today\'s Energy': 8.0,
'Battery Current': 14.0,
Expand Down Expand Up @@ -178,6 +156,48 @@
'Total Energy': 9
}

X3_HYBRID_G3_2X_MPPT_RESPONSE = {
"type": "X3-Hybiyd-G3",
"SN": "XXXXXXXXXX",
"ver": "2.033.20",
"Data": [0.0, 0.0, 0.0, 0.0, 0.9, 234.0, 3189, 42, 15.2, 27.0, -25, 0, 0,
210.30, -15.70, -3321, 24, 8.6, 0, 11.0, 1, 68, 232.4, 170.0,
31.0, 35.0, 22.6, 20.7, 3.8, 3.8, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,
2.03, 23.41, 123, 1344, 1722, 5.4, 6.9, 250.0, 251.9, 50.01,
50.01, 50.01, 0.0, 0.0, 0, 0.00, 0, 0, 0, 0.00, 0, 0, 0, 0, 0.00,
0, 0, 2, 1, 26, 1.00, 0, 100, 10, 25.00, 25.00, 0, 0, 0, 0, 0.0,
10.8, 24.4, 0.0, 0, 2, 0.0, 0.0, 0, 0.0, 0.0, 0, 0, 0, 0, 1, 1,
0, 0, 0.00, 0.00, 1, 273, 212.3, -16.2, -3439],
"Information": [8.000, 5, "X3-Hybiyd-G3", "XXXXXXXX", 1, 4.47, 0.00, 4.34,
1.05],
"battery": {
"brand": "83",
"masterVer": "1.11",
"slaveNum": "4",
"slaveVer": [1.13, 1.13, 1.13, 1.13]
}
}

X3_HYBRID_G3_2X_MPPT_RESPONSE_V34 = {
"type": 5,
"sn": "XXXXXXXXXX",
"ver": "2.034.06",
"Data": [2468, 2490, 2508, 13, 14, 10, 266, 284, 136, 5377, 4630, 17, 0,
958, 0, 5003, 5003, 5003, 2, 14833, 0, 103, 0, 0, 22930, 90, 229,
22, 99, 0, 7062, 0, 125, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
100, 0, 41, 7777, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65480,
0, 17372, 0, 59877, 0, 665, 41, 256, 2352, 1568, 20, 350, 202,
190, 41, 41, 81, 1, 1, 0, 0, 8142, 0, 17319, 0, 6, 0, 64851,
65535, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 83, 0, 55, 0, 0, 6, 0, 164,
43, 91, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 256, 12579, 783, 5381,
1107, 512, 8224, 8224, 0, 0, 4369, 0, 273, 2295, 5, 114, 4112,
4096, 25912, 31, 21302, 19778, 18003, 12355, 16697, 12354, 14132,
21302, 13110, 12338, 12337, 14386, 12354, 12852, 21302, 13110,
12338, 12337, 14386, 12354, 12340, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 686, 1, 257, 257, 1794, 1025, 0, 22930, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0],
"Information": [8.000, 5, "XXXXXXXX", 1, 4.47, 0.00, 4.34, 1.05, 0.0, 1]
}

X3_VALUES = {
'PV1 Current': 0,
Expand Down Expand Up @@ -267,6 +287,50 @@
'EPS Frequency': 0,
}

X3V34_HYBRID_VALUES = {
'Network Voltage Phase 1': 246.8,
'Network Voltage Phase 2': 249,
'Network Voltage Phase 3': 250.8,

'Output Current Phase 1': 1.3,
'Output Current Phase 2': 1.4,
'Output Current Phase 3': 1,

'Power Now Phase 1': 266,
'Power Now Phase 2': 284,
'Power Now Phase 3': 136,

'PV1 Voltage': 537.7,
'PV2 Voltage': 463,
'PV1 Current': 1.7,
'PV2 Current': 0,
'PV1 Power': 958,
'PV2 Power': 0,
'Total PV Power': 958,

'Grid Frequency Phase 1': 50.03,
'Grid Frequency Phase 2': 50.03,
'Grid Frequency Phase 3': 50.03,

'Total Energy': 1483.3,
'Today\'s Energy': 10.3,

'Battery Voltage': 229.3,
'Battery Current': 9,
'Battery Power': 229,
'Battery Temperature': 22,
'Battery Remaining Capacity': 99,

'Exported Power': -55,
'Total Feed-in Energy': 173.72,
'Total Feed-in Energy Resets': 0,
'Total Consumption': 598.77,
'Total Consumption Resets': 0,

'AC Power': 686,
'Load Power': 741,
}

X1_VALUES = {
'PV1 Current': 0,
'PV2 Current': 1,
Expand Down Expand Up @@ -406,6 +470,14 @@ def simple_http_fixture(httpserver):
response=X3_HYBRID_G3_2X_MPPT_RESPONSE,
inverter=inverter.X3,
values=X3_HYBRID_VALUES,
),
InverterUnderTest(
uri="/",
method='POST',
query_string='optType=ReadRealTimeData',
response=X3_HYBRID_G3_2X_MPPT_RESPONSE_V34,
inverter=inverter.X3V34,
values=X3V34_HYBRID_VALUES,
)
]

Expand Down

0 comments on commit 6d93b10

Please sign in to comment.