From ded7e25a044165a5e1304052a602390a1b1b9a76 Mon Sep 17 00:00:00 2001 From: Marc Luehr Date: Sun, 31 Mar 2024 09:30:04 +0200 Subject: [PATCH] feat: Add support for X3 Ultra --- setup.py | 1 + solax/inverters/__init__.py | 2 + solax/inverters/x3_ultra.py | 138 ++++++++++++++ tests/fixtures.py | 12 ++ tests/samples/expected_values.py | 63 +++++++ tests/samples/responses.py | 308 +++++++++++++++++++++++++++++++ 6 files changed, 524 insertions(+) create mode 100644 solax/inverters/x3_ultra.py diff --git a/setup.py b/setup.py index 515608c..067934b 100644 --- a/setup.py +++ b/setup.py @@ -41,6 +41,7 @@ "x1_smart = solax.inverters.x1_smart:X1Smart", "x3 = solax.inverters.x3:X3", "x3_hybrid_g4 = solax.inverters.x3_hybrid_g4:X3HybridG4", + "x3_ultra = solax.inverters.x3_ultra:X3Ultra", "x3_mic_pro_g2 = solax.inverters.x3_mic_pro_g2:X3MicProG2", "x3_v34 = solax.inverters.x3_v34:X3V34", "x_hybrid = solax.inverters.x_hybrid:XHybrid", diff --git a/solax/inverters/__init__.py b/solax/inverters/__init__.py index 883e9d3..039c947 100644 --- a/solax/inverters/__init__.py +++ b/solax/inverters/__init__.py @@ -8,6 +8,7 @@ from .x3 import X3 from .x3_hybrid_g4 import X3HybridG4 from .x3_mic_pro_g2 import X3MicProG2 +from .x3_ultra import X3Ultra from .x3_v34 import X3V34 from .x_hybrid import XHybrid @@ -24,4 +25,5 @@ "X1Boost", "X1HybridGen4", "X3MicProG2", + "X3Ultra", ] diff --git a/solax/inverters/x3_ultra.py b/solax/inverters/x3_ultra.py new file mode 100644 index 0000000..43488dd --- /dev/null +++ b/solax/inverters/x3_ultra.py @@ -0,0 +1,138 @@ +from typing import Any, Dict, Optional + +import voluptuous as vol + +from solax.inverter import Inverter +from solax.units import DailyTotal, Measurement, Total, Units +from solax.utils import ( + div10, + div100, + pack_u16, + to_signed, + to_signed32, + twoway_div10, + twoway_div100, +) + + +class X3Ultra(Inverter): + """X3 Ultra v1.001.20""" + + # pylint: disable=duplicate-code + _schema = vol.Schema( + { + vol.Required("type"): vol.All(int, 25), + vol.Required("sn"): str, + vol.Required("ver"): str, + vol.Required("data"): vol.Schema( + vol.All( + [vol.Coerce(float)], + vol.Length(min=300, max=300), + ) + ), + vol.Required("information"): vol.Schema( + vol.All(vol.Length(min=10, max=10)) + ), + }, + extra=vol.REMOVE_EXTRA, + ) + + @classmethod + def build_all_variants(cls, host, port, pwd=""): + return [cls._build(host, port, pwd, False)] + + @classmethod + def _decode_run_mode(cls, run_mode): + return { + 0: "Waiting", + 1: "Checking", + 2: "Normal", + 3: "Fault", + 4: "Permanent Fault", + 5: "Updating", + 6: "EPS Check", + 7: "EPS Mode", + 8: "Self Test", + 9: "Idle", + 10: "Standby", + }.get(run_mode) + + @classmethod + def response_decoder(cls): + return { + "Grid 1 Voltage": (0, Units.V, div10), + "Grid 2 Voltage": (1, Units.V, div10), + "Grid 3 Voltage": (2, Units.V, div10), + "Grid 1 Current": (3, Units.A, twoway_div10), + "Grid 2 Current": (4, Units.A, twoway_div10), + "Grid 3 Current": (5, Units.A, twoway_div10), + "Grid 1 Power": (6, Units.W, to_signed), + "Grid 2 Power": (7, Units.W, to_signed), + "Grid 3 Power": (8, Units.W, to_signed), + "PV1 Voltage": (10, Units.V, div10), + "PV2 Voltage": (11, Units.V, div10), + "PV3 Voltage": (129, Units.V, div10), + "PV1 Current": (12, Units.A, div10), + "PV2 Current": (13, Units.A, div10), + "PV3 Current": (130, Units.A, div10), + "PV1 Power": (14, Units.W), + "PV2 Power": (15, Units.W), + "PV3 Power": (131, Units.W), + "Grid 1 Frequency": (16, Units.HZ, div100), + "Grid 2 Frequency": (17, Units.HZ, div100), + "Grid 3 Frequency": (18, Units.HZ, div100), + # "Run mode": (19, Units.NONE), # Only use the index once due to HA uids + "Run mode text": (19, Units.NONE, X3Ultra._decode_run_mode), + "EPS 1 Voltage": (23, Units.V, div10), + "EPS 2 Voltage": (24, Units.V, div10), + "EPS 3 Voltage": (25, Units.V, div10), + "EPS 1 Current": (26, Units.A, twoway_div10), + "EPS 2 Current": (27, Units.A, twoway_div10), + "EPS 3 Current": (28, Units.A, twoway_div10), + "EPS 1 Power": (29, Units.W, to_signed), + "EPS 2 Power": (30, Units.W, to_signed), + "EPS 3 Power": (31, Units.W, to_signed), + "Grid Power ": (pack_u16(34, 35), Units.W, to_signed32), + "Battery 1 Voltage": (39, Units.V, div10), + "Battery 2 Voltage": (132, Units.V, div10), + "Battery 1 Current": (40, Units.A, twoway_div100), + "Battery 2 Current": (133, Units.A, twoway_div100), + "Battery 1 Power": (41, Units.W, to_signed), + "Battery 2 Power": (134, Units.W, to_signed), + "Battery 1 Remaining Capacity": (103, Units.PERCENT), + "Battery 2 Remaining Capacity": (140, Units.PERCENT), + "Battery 1 Temperature": (105, Units.C, to_signed), + "Battery 2 Temperature": (142, Units.C, to_signed), + "Load/Generator Power": (47, Units.W, to_signed), + "Radiator Temperature": (54, Units.C, to_signed), + "Yield total": (pack_u16(58, 59), Total(Units.KWH), div10), + "Yield today": (70, DailyTotal(Units.KWH), div10), + "Battery Discharge Energy total": ( + pack_u16(74, 75), + Total(Units.KWH), + div10, + ), + "Battery Charge Energy total": (pack_u16(76, 77), Total(Units.KWH), div10), + "Battery Discharge Energy today": (78, DailyTotal(Units.KWH), div10), + "Battery Charge Energy today": (79, DailyTotal(Units.KWH), div10), + "PV Energy total": (pack_u16(80, 81), Total(Units.KWH), div10), + "EPS Energy total": (pack_u16(83, 84), Total(Units.KWH), div10), + "EPS Energy today": (85, DailyTotal(Units.KWH), div10), + "Feed-in Energy total": (pack_u16(86, 87), Total(Units.KWH), div100), + "Grid Consumed Energy total": (pack_u16(88, 89), Total(Units.KWH), div100), + "Feed-in Energy today": (pack_u16(90, 91), DailyTotal(Units.KWH), div100), + "Grid Consumed Energy today": ( + pack_u16(92, 93), + DailyTotal(Units.KWH), + div100, + ), + "Battery Remaining Capacity": (158, Units.PERCENT), + "Battery Remaining Energy": (106, Measurement(Units.KWH), div10), + "Inverter Power": (159, Units.W, div10), + } + + # pylint: enable=duplicate-code + + @classmethod + def inverter_serial_number_getter(cls, response: Dict[str, Any]) -> Optional[str]: + return response["information"][2] diff --git a/tests/fixtures.py b/tests/fixtures.py index 3983bfb..7cf9d07 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -16,6 +16,7 @@ X3_HYBRID_G4_VALUES, X3_HYBRID_VALUES, X3_MICPRO_G2_VALUES, + X3_ULTRA_VALUES, X3_VALUES, X3V34_HYBRID_VALUES, X3V34_HYBRID_VALUES_EPS_MODE, @@ -41,6 +42,7 @@ X3_HYBRID_G4_RESPONSE, X3_MIC_RESPONSE, X3_MICPRO_G2_RESPONSE, + X3_ULTRA_RESPONSE, XHYBRID_DE01_RESPONSE, XHYBRID_DE02_RESPONSE, ) @@ -242,6 +244,16 @@ def simple_http_fixture(httpserver): headers=None, data="optType=ReadRealTimeData", ), + InverterUnderTest( + uri="/", + method="POST", + query_string=None, + response=X3_ULTRA_RESPONSE, + inverter=inverter.X3Ultra, + values=X3_ULTRA_VALUES, + headers=None, + data="optType=ReadRealTimeData", + ), InverterUnderTest( uri="/", method="POST", diff --git a/tests/samples/expected_values.py b/tests/samples/expected_values.py index 4af3422..95f8924 100644 --- a/tests/samples/expected_values.py +++ b/tests/samples/expected_values.py @@ -521,3 +521,66 @@ "Total feed-in energy": 286.7, "Total consumption": 6.2, } + +X3_ULTRA_VALUES = { + "Grid 1 Voltage": 232.8, + "Grid 2 Voltage": 233.8, + "Grid 3 Voltage": 232.0, + "Grid 1 Current": 2.0, + "Grid 2 Current": 1.9, + "Grid 3 Current": 1.9, + "Grid 1 Power": 216.0, + "Grid 2 Power": 249.0, + "Grid 3 Power": 199.0, + "PV1 Voltage": 0.0, + "PV2 Voltage": 249.8, + "PV3 Voltage": 245.4, + "PV1 Current": 0.0, + "PV2 Current": 6.5, + "PV3 Current": 10.5, + "PV1 Power": 0.0, + "PV2 Power": 1629.0, + "PV3 Power": 2592.0, + "Grid 1 Frequency": 50.0, + "Grid 2 Frequency": 50.0, + "Grid 3 Frequency": 50.0, + "Run mode text": "Normal", + "EPS 1 Voltage": 0.0, + "EPS 2 Voltage": 0.0, + "EPS 3 Voltage": 0.0, + "EPS 1 Current": 0.0, + "EPS 2 Current": 0.0, + "EPS 3 Current": 0.0, + "EPS 1 Power": 0.0, + "EPS 2 Power": 0.0, + "EPS 3 Power": 0.0, + "Grid Power ": 52.0, + "Battery 1 Voltage": 213.8, + "Battery 2 Voltage": 212.9, + "Battery 1 Current": 9.6, + "Battery 2 Current": 7.4, + "Battery 1 Power": 2057.0, + "Battery 2 Power": 1595.0, + "Battery 1 Remaining Capacity": 28.0, + "Battery 2 Remaining Capacity": 28.0, + "Battery 1 Temperature": 13.0, + "Battery 2 Temperature": 12.0, + "Load/Generator Power": -52.0, + "Radiator Temperature": 34.0, + "Yield total": 198.4, + "Yield today": 2.4, + "Battery Discharge Energy total": 109.2, + "Battery Charge Energy total": 122.8, + "Battery Discharge Energy today": 0.2, + "Battery Charge Energy today": 2.7, + "PV Energy total": 122.9, + "EPS Energy total": 0.0, + "EPS Energy today": 0.0, + "Feed-in Energy total": 27.71, + "Grid Consumed Energy total": 128.12, + "Feed-in Energy today": 0.1, + "Grid Consumed Energy today": 8.6, + "Battery Remaining Capacity": 28.0, + "Battery Remaining Energy": 1.7, + "Inverter Power": 66.4, +} diff --git a/tests/samples/responses.py b/tests/samples/responses.py index 0e4078f..4f04971 100644 --- a/tests/samples/responses.py +++ b/tests/samples/responses.py @@ -3031,6 +3031,314 @@ "Information": [4.000, 16, "MC20XXXXXXXXXX", 8, 1.20, 0.00, 1.18, 1.00, 0.00, 1], } +X3_ULTRA_RESPONSE = { + "sn": "SNXXXXXXXX", + "ver": "1.001.20", + "type": 25, + "Data": [ + 2328, + 2338, + 2320, + 20, + 19, + 19, + 216, + 249, + 199, + 0, + 0, + 2498, + 0, + 65, + 0, + 1629, + 5000, + 5000, + 5000, + 2, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 52, + 0, + 0, + 0, + 0, + 2138, + 960, + 2057, + 2159, + 78, + 0, + 1, + 27, + 65484, + 256, + 6159, + 7436, + 6147, + 100, + 0, + 34, + 0, + 0, + 0, + 1984, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 24, + 23, + 0, + 1, + 1092, + 0, + 1228, + 0, + 2, + 27, + 1229, + 0, + 26, + 0, + 0, + 0, + 2771, + 0, + 12812, + 0, + 10, + 0, + 860, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 28, + 1, + 13, + 17, + 256, + 2336, + 1600, + 120, + 234, + 138, + 125, + 34, + 34, + 9, + 3378, + 3366, + 60289, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 2454, + 105, + 2592, + 2129, + 740, + 1595, + 2159, + 78, + 0, + 0, + 1, + 28, + 1, + 12, + 17, + 256, + 2336, + 1600, + 120, + 234, + 136, + 121, + 34, + 34, + 9, + 3379, + 3368, + 3652, + 0, + 28, + 664, + 0, + 62029, + 1, + 1684, + 0, + 0, + 0, + 1684, + 0, + 20564, + 12339, + 19009, + 12866, + 16693, + 12612, + 14646, + 20564, + 12339, + 19009, + 12866, + 16693, + 12612, + 13880, + 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, + 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, + 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, + 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": [25.000, 25, "H3BC25XXXXXXXX", 13, 4.06, 0.00, 4.04, 0.04, 0.00, 1], +} QVOLTHYBG33P_RESPONSE_V34 = { "sn": "SWXXXX", "ver": "2.034.06",