From eefc4469b95d275c2e8acdfc57cc25b30685153a Mon Sep 17 00:00:00 2001 From: Vadim Kraus <38394456+VadimKraus@users.noreply.github.com> Date: Mon, 21 Feb 2022 21:03:00 +0100 Subject: [PATCH] Add Support for Q.VOLT HYB-G3-3P (#57) * Add QVOLTHYBG33P Inverter * Use Post with body for QVOLTHYBG33P * Fix kwh typo * Fix linting issues * Move inverter/battery modes to QVOLTHYBG33P Co-authored-by: Kraus, Vadim --- solax/inverter.py | 207 +++++++++++++++++++++++++++++++++++++++++++++- tests/fixtures.py | 79 +++++++++++++++++- 2 files changed, 282 insertions(+), 4 deletions(-) diff --git a/solax/inverter.py b/solax/inverter.py index efaab6f..553ac4b 100644 --- a/solax/inverter.py +++ b/solax/inverter.py @@ -413,6 +413,210 @@ def schema(cls): return cls.__schema +class QVOLTHYBG33P(InverterPost): + """ + QCells + Q.VOLT HYB-G3-3P + """ + class Processors: + """ + Postprocessors used only in the QVOLTHYBG33P inverter sensor_map. + """ + @staticmethod + def inverter_modes(value, *_args, **_kwargs): + return { + 0: "Waiting", + 1: "Checking", + 2: "Normal", + 3: "Off", + 4: "Permanent Fault", + 5: "Updating", + 6: "EPS Check", + 7: "EPS Mode", + 8: "Self Test", + 9: "Idle", + 10: "Standby" + }.get(value, f"unmapped value '{value}'") + + @staticmethod + def battery_modes(value, *_args, **_kwargs): + return { + 0: "Self Use Mode", + 1: "Force Time Use", + 2: "Back Up Mode", + 3: "Feed-in Priority", + }.get(value, f"unmapped value '{value}'") + + __schema = vol.Schema({ + vol.Required('type'): vol.All(int, 14), + 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', twoway_div10), + 'Output Current Phase 2': (4, 'A', twoway_div10), + 'Output Current Phase 3': (5, 'A', twoway_div10), + + 'Power Now Phase 1': (6, 'W', to_signed), + 'Power Now Phase 2': (7, 'W', to_signed), + 'Power Now Phase 3': (8, 'W', to_signed), + + 'AC Power': (9, 'W', to_signed), + + 'PV1 Voltage': (10, 'V', div10), + 'PV2 Voltage': (11, 'V', div10), + + 'PV1 Current': (12, 'A', div10), + 'PV2 Current': (13, 'A', div10), + + 'PV1 Power': (14, 'W'), + 'PV2 Power': (15, 'W'), + + 'Grid Frequency Phase 1': (16, 'Hz', div100), + 'Grid Frequency Phase 2': (17, 'Hz', div100), + 'Grid Frequency Phase 3': (18, 'Hz', div100), + + 'Inverter Operation mode': (19, '', + Processors.inverter_modes), + # 20 - 32: always 0 + # 33: always 1 + # instead of to_signed this is actually 34 - 35, + # because 35 = if 34>32767: 0 else: 65535 + 'Exported Power': (34, 'W', to_signed), + # 35: if 34>32767: 0 else: 65535 + # 36 - 38 : always 0 + 'Battery Voltage': (39, 'V', div100), + 'Battery Current': (40, 'A', twoway_div100), + 'Battery Power': (41, 'W', to_signed), + # 42: div10, almost identical to [39] + # 43: twoway_div10, almost the same as "40" (battery current) + # 44: twoway_div100, almost the same as "41" (battery power), + # 45: always 1 + # 46: follows PV Output, idles around 44, peaks at 52, + 'Power Now': (47, 'W', to_signed), + # 48: always 256 + # 49,50: [49] + [50] * 15160 some increasing counter + # 51: always 5634 + # 52: always 100 + # 53: always 0 + # 54: follows PV Output, idles around 35, peaks at 54, + # 55-67: always 0 + 'Total Energy': (68, 'kWh', total_energy), + 'Total Energy Resets': (69, ''), + # 70: div10, today's energy including battery usage + # 71-73: 0 + 'Total Battery Discharge Energy': (74, 'kWh', discharge_energy), + 'Total Battery Discharge Energy Resets': (75, ''), + 'Total Battery Charge Energy': (76, 'kWh', charge_energy), + 'Total Battery Charge Energy Resets': (77, ''), + 'Today\'s Battery Discharge Energy': (78, 'kWh', div10), + 'Today\'s Battery Charge Energy': (79, 'kWh', div10), + 'Total PV Energy': (80, 'kWh', pv_energy), + 'Total PV Energy Resets': (81, ''), + 'Today\'s Energy': (82, 'kWh', div10), + # 83-85: always 0 + 'Total Feed-in Energy': (86, 'kWh', feedin_energy), + 'Total Feed-in Energy Resets': (87, ''), + 'Total Consumption': (88, 'kWh', consumption), + 'Total Consumption Resets': (89, ''), + 'Today\'s Feed-in Energy': (90, 'kWh', div100), + # 91: always 0 + 'Today\'s Consumption': (92, 'kWh', div100), + # 93-101: always 0 + # 102: always 1 + 'Battery Remaining Capacity': (103, '%'), + # 104: always 1 + 'Battery Temperature': (105, 'C'), + 'Battery Remaining Energy': (106, 'kWh', div10), + # 107: always 256 or 0 + # 108: always 3504 + # 109: always 2400 + # 110: around rise to 300 if battery not full, 0 if battery is full + # 112, 113: range [250,350]; looks like 113 + offset = 112, + # peaks if battery is full + # 114, 115: something around 33; Some temperature?! + # 116: increases slowly [2,5] + # 117-121: 1620 773 12850 12850 12850 + # 122-124: always 0 + # 125,126: some curve, look very similar to "42"(Battery Power), + # with offset around 15 + # 127,128 resetting counter /1000, around battery charge + discharge + # 164,165,166 some curves + 'Battery Operation mode': (168, '', + Processors.battery_modes), + # 169: div100 same as [39] + # 170-199: always 0 + + } + + @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 + + @classmethod + async def make_request(cls, host, port=80, pwd='', headers=None): + + url = f'http://{host}:{port}/' + data = f'optType=ReadRealTimeData&pwd={pwd}' + + async with aiohttp.ClientSession() as session: + async with session.post(url, headers=headers, data=data) as req: + resp = await req.read() + + raw_json = resp.decode("utf-8") + json_response = json.loads(raw_json) + response = {} + try: + response = cls.schema()(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'] + ) + + class X1(InverterPost): __schema = vol.Schema({ vol.Required('type'): vol.All( @@ -675,4 +879,5 @@ def schema(cls): # registry of inverters -REGISTRY = [XHybrid, X3, X3V34, X1, X1Mini, X1MiniV34, X1Smart] +REGISTRY = [XHybrid, X3, X3V34, X1, X1Mini, X1MiniV34, X1Smart, + QVOLTHYBG33P] diff --git a/tests/fixtures.py b/tests/fixtures.py index d5a0694..0eee620 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -280,6 +280,28 @@ "Information": [8.000, 5, "XXXXXXXX", 1, 4.60, 0.00, 4.42, 1.05, 0.00, 1] } +QVOLTHYBG33P_RESPONSE_V34 = { + "sn": "SWX***", + "ver": "2.034.06", + "type": 14, + "Data": [ + 2214, 2238, 2251, 11, 10, 12, 162, 136, 146, 444, 5662, 5682, 18, 17, + 1050, 977, 5002, 5001, 5002, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 65529, 65535, 0, 0, 0, 32340, 500, 1616, 3236, 50, 1618, 1, 50, 451, + 256, 3841, 4875, 5634, 100, 0, 45, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 2190, 0, 36, 1, 0, 1, 738, 0, 904, 0, 0, 81, 2316, 0, 118, 0, 0, 0, + 10794, 0, 14544, 0, 166, 0, 466, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 95, 1, + 35, 88, 256, 3504, 2400, 115, 300, 352, 325, 34, 34, 8, 1620, 773, + 12850, 12850, 12850, 0, 0, 0, 3389, 3384, 33876, 2, 20564, 12339, + 18497, 12599, 18743, 12356, 14386, 20564, 12339, 18498, 12600, 18740, + 12356, 12855, 20564, 12339, 18498, 12600, 18740, 12612, 14642, 20564, + 12339, 18498, 12600, 18740, 12356, 13877, 0, 0, 0, 0, 0, 0, 0, 3843, + 2306, 1282, 257, 0, 32340, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ], + "Information": [12.0, 14, "H34***", 1, 1.15, 0.0, 1.14, 1.07, 0.0, 1] +} + X3_VALUES = { 'PV1 Current': 0, 'PV2 Current': 1, @@ -317,8 +339,6 @@ 'Power Now Phase 2': 44, 'Power Now Phase 3': 45, - - 'EPS Voltage': 53, 'EPS Current': 54, 'EPS Power': 55, @@ -641,6 +661,50 @@ 'Total Consumption': 81.84, } +QVOLTHYBG33P_VALUES = { + 'Network Voltage Phase 1': 221.4, + 'Network Voltage Phase 2': 223.8, + 'Network Voltage Phase 3': 225.1, + 'Output Current Phase 1': 1.1, + 'Output Current Phase 2': 1.0, + 'Output Current Phase 3': 1.2, + 'Power Now Phase 1': 162.0, + 'Power Now Phase 2': 136.0, + 'Power Now Phase 3': 146.0, + 'AC Power': 444.0, + 'PV1 Voltage': 566.2, 'PV2 Voltage': 568.2, + 'PV1 Current': 1.8, 'PV2 Current': 1.7, + 'PV1 Power': 1050.0, + 'PV2 Power': 977.0, 'Grid Frequency Phase 1': 50.02, + 'Grid Frequency Phase 2': 50.01, + 'Grid Frequency Phase 3': 50.02, + 'Inverter Operation mode': 'Normal', + 'Exported Power': -6.0, 'Battery Voltage': 323.4, + 'Battery Current': 5.0, + 'Battery Power': 1616.0, 'Power Now': 451.0, + 'Total Energy': 219.0, + 'Total Energy Resets': 0.0, + 'Total Battery Discharge Energy': 73.8, + 'Total Battery Discharge Energy Resets': 0.0, + 'Total Battery Charge Energy': 90.4, + 'Total Battery Charge Energy Resets': 0.0, + "Today's Battery Discharge Energy": 0.0, + "Today's Battery Charge Energy": 8.1, + 'Total PV Energy': 231.6, + 'Total PV Energy Resets': 0.0, + "Today's Energy": 11.8, + 'Total Feed-in Energy': 107.94, + 'Total Feed-in Energy Resets': 0.0, + 'Total Consumption': 145.44, + 'Total Consumption Resets': 0.0, + "Today's Feed-in Energy": 1.66, + "Today's Consumption": 4.66, + 'Battery Remaining Capacity': 95.0, + 'Battery Temperature': 35.0, + 'Battery Remaining Energy': 8.8, + 'Battery Operation mode': 'Self Use Mode' +} + @pytest.fixture() def simple_http_fixture(httpserver): @@ -774,7 +838,16 @@ def simple_http_fixture(httpserver): inverter=inverter.X3V34, values=X3V34_HYBRID_VALUES_EPS_MODE, headers=None, - ) + ), + InverterUnderTest( + uri="/", + method='POST', + query_string='', + response=QVOLTHYBG33P_RESPONSE_V34, + inverter=inverter.QVOLTHYBG33P, + values=QVOLTHYBG33P_VALUES, + headers=None, + ), ]