From 6d93b103937ed672c4a86eafdbdd22bd9a116b90 Mon Sep 17 00:00:00 2001 From: Petru Paler Date: Mon, 16 Aug 2021 04:01:54 +0100 Subject: [PATCH] Add support for X3 hybrid firmware version v2.034.06 (#37) --- solax/inverter.py | 176 ++++++++++++++++++++++++++++++++++++++++++---- tests/fixtures.py | 116 ++++++++++++++++++++++++------ 2 files changed, 257 insertions(+), 35 deletions(-) diff --git a/solax/inverter.py b/solax/inverter.py index 0a94f37..a0ce9ad 100644 --- a/solax/inverter.py +++ b/solax/inverter.py @@ -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): @@ -57,6 +58,13 @@ 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): """ @@ -64,13 +72,18 @@ def schema(cls): """ 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: @@ -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'] @@ -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'] ) @@ -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( @@ -411,4 +561,4 @@ def schema(cls): # registry of inverters -REGISTRY = [XHybrid, X3, X1, X1Mini] +REGISTRY = [XHybrid, X3, X3V34, X1, X1Mini] diff --git a/tests/fixtures.py b/tests/fixtures.py index e69467a..300f82a 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -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, @@ -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, @@ -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, @@ -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, ) ]