From 90b0b5ce6516c11868bcef9b349179a2855e73fb Mon Sep 17 00:00:00 2001 From: panjialiang <371630856@qq.com> Date: Tue, 19 Dec 2023 19:54:18 +0800 Subject: [PATCH 1/3] add new inverter and AES encryption --- setup.py | 1 + solax/data_encryption.py | 110 ++++++++++++++++++++++++++ solax/discovery.py | 22 +++++- solax/inverter.py | 19 +++-- solax/inverters/__init__.py | 18 +++++ solax/inverters/j1_ess_hb.py | 86 +++++++++++++++++++++ solax/inverters/x1_boost_g4.py | 89 +++++++++++++++++++++ solax/inverters/x1_hybrid_g2.py | 112 +++++++++++++++++++++++++++ solax/inverters/x1_hybrid_gen4.py | 77 +++++++++++++------ solax/inverters/x1_hybrid_lv.py | 103 +++++++++++++++++++++++++ solax/inverters/x1_ies.py | 104 +++++++++++++++++++++++++ solax/inverters/x1_mini_g3.py | 108 ++++++++++++++++++++++++++ solax/inverters/x1_mini_g4.py | 91 ++++++++++++++++++++++ solax/inverters/x3_fth.py | 89 +++++++++++++++++++++ solax/inverters/x3_hybrid_g4.py | 42 ++++++---- solax/inverters/x3_ies.py | 122 +++++++++++++++++++++++++++++ solax/inverters/x3_mga_g2.py | 89 +++++++++++++++++++++ solax/inverters/x3_mic_pro_g2.py | 3 + solax/inverters/x3_ultra.py | 124 ++++++++++++++++++++++++++++++ 19 files changed, 1364 insertions(+), 45 deletions(-) create mode 100644 solax/data_encryption.py create mode 100644 solax/inverters/j1_ess_hb.py create mode 100644 solax/inverters/x1_boost_g4.py create mode 100644 solax/inverters/x1_hybrid_g2.py create mode 100644 solax/inverters/x1_hybrid_lv.py create mode 100644 solax/inverters/x1_ies.py create mode 100644 solax/inverters/x1_mini_g3.py create mode 100644 solax/inverters/x1_mini_g4.py create mode 100644 solax/inverters/x3_fth.py create mode 100644 solax/inverters/x3_ies.py create mode 100644 solax/inverters/x3_mga_g2.py create mode 100644 solax/inverters/x3_ultra.py diff --git a/setup.py b/setup.py index 759b61e..3b3f707 100644 --- a/setup.py +++ b/setup.py @@ -18,6 +18,7 @@ "aiohttp>=3.5.4, <4", "async_timeout>=4.0.2", "voluptuous>=0.11.5", + "pycryptodome>=3.19.0", ], setup_requires=[ "setuptools_scm", diff --git a/solax/data_encryption.py b/solax/data_encryption.py new file mode 100644 index 0000000..c3255d9 --- /dev/null +++ b/solax/data_encryption.py @@ -0,0 +1,110 @@ +import json +import aiohttp +import base64 + +from Crypto.Cipher import AES +from Crypto.Util.Padding import pad, unpad + + +class DataEncrypt: + """ + The real-time data needs to be AES encrypted + """ + + def __init__(self, serial_number: str, url: str): + self.url = url + self.iv = bytes( + [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f]) + self.serial_number = str(serial_number) + self.login_request_body = "optType=newParaSetting&subOption=pwd&Value=" + self.serial_number + self.real_time_body = "optType=ReadRealTimeData&pwd=" + self.serial_number + self.key = "" + self.token = "" + + async def encrypt(self, plain_text: bytes): + aes = AES.new(self.key, AES.MODE_CBC, self.iv) + cipher_text = aes.encrypt(pad(plain_text, AES.block_size)) + return cipher_text + + async def decrypt(self, cipher_text: bytes): + aes = AES.new(self.key, AES.MODE_CBC, self.iv) + decrypted_text = unpad(aes.decrypt(cipher_text), AES.block_size) + return decrypted_text + + async def get_token_key(self, datahub_sn: str): + """ + Generate AES encrypted key based on SN number + """ + ret = '' + mqtt_login_password = [0] * 8 + mqtt_login_password[0] = datahub_sn[7] + mqtt_login_password[1] = datahub_sn[4] + mqtt_login_password[2] = datahub_sn[3] + mqtt_login_password[3] = datahub_sn[6] + mqtt_login_password[4] = datahub_sn[5] + mqtt_login_password[5] = datahub_sn[2] + mqtt_login_password[6] = datahub_sn[9] + mqtt_login_password[7] = datahub_sn[8] + for i in range(0, len(mqtt_login_password)): + mqtt_login_password[i] = chr(ord(mqtt_login_password[i]) ^ 11) + if mqtt_login_password[i].isalpha() or mqtt_login_password[i].isdigit(): + ret += mqtt_login_password[i] + else: + ret += 'A' + + return ret.encode('utf-8').hex() + + async def fill_16_byte(self, original_hex: str): + """ + Complete the string length to 16 bits + """ + byte_string = bytes.fromhex(original_hex) + padding_bytes = 16 - len(byte_string) + padded_byte_string = byte_string + bytes([0] * padding_bytes) + padded_hex_string = padded_byte_string.hex() + return bytes.fromhex(padded_hex_string) + + async def get_token(self, encodebytes: bytes): + decrypted_text = await self.decrypt(encodebytes) + res_data = decrypted_text.decode() + res_data_json = json.loads(res_data) + self.token = res_data_json.get("data").get("token") + + async def get_real_time(self) -> bytes: + headers = { + "token": self.token, + "Content-Type": "application/json" + } + real_time_text = await self.encrypt(self.real_time_body.encode('utf8')) + + real_time_base64 = base64.b64encode(real_time_text) + + real_time_data = real_time_base64.decode('utf8') + async with aiohttp.ClientSession() as session: + async with session.post(self.url, headers=headers, data=real_time_data) as req: + response = await req.text() + + real_time = response + encode_bytes = base64.decodebytes(real_time.encode('utf-8')) + decrypted_text = await self.decrypt(encode_bytes) + + return decrypted_text + + async def get_encrypt_data(self) -> bytes: + """ + Decrypt data By AES + """ + self.key = await self.fill_16_byte(await self.get_token_key(self.serial_number)) + + encrypt_text = await self.encrypt(self.login_request_body.encode('utf8')) + + encode_strs = base64.b64encode(encrypt_text) + + login_data = encode_strs.decode('utf8') + async with aiohttp.ClientSession() as session: + async with session.post(self.url, data=login_data) as req: + response = await req.text() + + encodebytes = base64.decodebytes(response.encode('utf-8')) + await self.get_token(encodebytes) + return await self.get_real_time() diff --git a/solax/discovery.py b/solax/discovery.py index 9cfdd60..ae35493 100644 --- a/solax/discovery.py +++ b/solax/discovery.py @@ -16,6 +16,15 @@ X3HybridG4, X3MicProG2, XHybrid, + J1EssHb, + X1HybridG2, + X1MiniG3, + X1MiniG4, + X1HybridLv, + X1Ies, + X3Ies, + X3Ultra, + X1BoostG4 ) # registry of inverters @@ -32,9 +41,17 @@ X1Boost, X1HybridGen4, X3MicProG2, + J1EssHb, + X1HybridG2, + X1MiniG3, + X1MiniG4, + X1HybridLv, + X1Ies, + X3Ies, + X3Ultra, + X1BoostG4 ] - logging.basicConfig(level=logging.INFO) @@ -66,7 +83,8 @@ def _task_handler(self, task): @classmethod async def _discovery_task(cls, i) -> Inverter: logging.info("Trying inverter %s", i) - await i.get_data() + final_data = await i.get_data() + return i async def discover(self, host, port, pwd="") -> Inverter: diff --git a/solax/inverter.py b/solax/inverter.py index df3042c..7a7cf78 100644 --- a/solax/inverter.py +++ b/solax/inverter.py @@ -7,6 +7,7 @@ from solax.inverter_http_client import InverterHttpClient, Method from solax.response_parser import InverterResponse, ResponseDecoder, ResponseParser from solax.units import Measurement, Units +from solax.data_encryption import DataEncrypt class InverterError(Exception): @@ -26,9 +27,11 @@ def response_decoder(cls) -> ResponseDecoder: # pylint: enable=C0301 _schema = vol.Schema({}) # type: vol.Schema + url: str = '' + pwd: str = '' def __init__( - self, http_client: InverterHttpClient, response_parser: ResponseParser + self, http_client: InverterHttpClient, response_parser: ResponseParser ): self.manufacturer = "Solax" self.response_parser = response_parser @@ -36,8 +39,9 @@ def __init__( @classmethod def _build(cls, host, port, pwd="", params_in_query=True): - url = utils.to_url(host, port) - http_client = InverterHttpClient(url, Method.POST, pwd) + cls.url = utils.to_url(host, port) + cls.pwd = pwd + http_client = InverterHttpClient(cls.url, Method.POST, cls.pwd) if params_in_query: http_client.with_default_query() else: @@ -73,6 +77,13 @@ async def make_request(self) -> InverterResponse: Raise exception if unable to get data """ raw_response = await self.http_client.request() + str_raw_response = raw_response.decode('utf-8') + if str_raw_response.startswith('{"code":'): + pass + else: + if not str_raw_response.startswith('{"sn"'): + raw_response = await DataEncrypt(self.pwd, self.url).get_encrypt_data() + return self.response_parser.handle_response(raw_response) @classmethod @@ -84,9 +95,7 @@ def sensor_map(cls) -> Dict[str, Tuple[int, Measurement]]: sensors: Dict[str, Tuple[int, Measurement]] = {} for name, mapping in cls.response_decoder().items(): unit = Measurement(Units.NONE) - (idx, unit_or_measurement, *_) = mapping - if isinstance(unit_or_measurement, Units): unit = Measurement(unit_or_measurement) else: diff --git a/solax/inverters/__init__.py b/solax/inverters/__init__.py index 883e9d3..e55eff2 100644 --- a/solax/inverters/__init__.py +++ b/solax/inverters/__init__.py @@ -10,6 +10,15 @@ from .x3_mic_pro_g2 import X3MicProG2 from .x3_v34 import X3V34 from .x_hybrid import XHybrid +from .j1_ess_hb import J1EssHb +from .x1_hybrid_g2 import X1HybridG2 +from .x1_mini_g4 import X1MiniG4 +from .x1_mini_g3 import X1MiniG3 +from .x1_hybrid_lv import X1HybridLv +from .x1_ies import X1Ies +from .x3_ies import X3Ies +from .x3_ultra import X3Ultra +from .x1_boost_g4 import X1BoostG4 __all__ = [ "QVOLTHYBG33P", @@ -24,4 +33,13 @@ "X1Boost", "X1HybridGen4", "X3MicProG2", + "J1EssHb", + "X1HybridG2", + "X1MiniG3", + "X1MiniG4", + "X1HybridLv", + "X1Ies", + "X3Ies", + "X3Ultra", + "X1BoostG4" ] diff --git a/solax/inverters/j1_ess_hb.py b/solax/inverters/j1_ess_hb.py new file mode 100644 index 0000000..32ef899 --- /dev/null +++ b/solax/inverters/j1_ess_hb.py @@ -0,0 +1,86 @@ +import voluptuous as vol + +from solax.inverter import Inverter +from solax.units import Total, Units +from solax.utils import div10, div100, pack_u16, to_signed, to_signed32, twoway_div10 + + +class J1EssHb(Inverter): + # pylint: disable=duplicate-code + _schema = vol.Schema( + { + vol.Required("type"): int, + 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 1 Current": (1, Units.A, div10), + "Grid 1 Power": (2, Units.W, to_signed), + "Grid 2 Voltage": (3, Units.V, div10), + "Grid 2 Current": (4, Units.A, div10), + "Grid 2 Power": (5, Units.W, to_signed), + "Grid Power Total": (6, Units.W, to_signed), + "Grid Frequency": (7, Units.HZ, div100), + "PV1 Voltage": (8, Units.V, div10), + "PV1 Current": (9, Units.A, div10), + "PV1 Power": (10, Units.W), + "PV2 Voltage": (11, Units.V, div10), + "PV2 Current": (12, Units.A, div10), + "PV2 Power": (13, Units.W), + "PV3 Voltage": (14, Units.V, div10), + "PV3 Current": (15, Units.A, div10), + "PV3 Power": (16, Units.W), + "Run Mode": (17, Units.NONE, J1EssHb._decode_run_mode), + "EPS 1 Voltage": (19, Units.V, div10), + "EPS 1 Current": (20, Units.A, twoway_div10), + "EPS 1 Power": (21, Units.W, to_signed), + "EPS 2 Voltage": (23, Units.V, div10), + "EPS 2 Current": (24, Units.A, twoway_div10), + "EPS 2 Power": (25, Units.W, to_signed), + "EPS Frequency": (26, Units.HZ, div100), + "Feed-in 1 Power ": (27, Units.W, to_signed), + "Feed-in 2 Power ": (28, Units.W, to_signed), + "Feed-in Power Total ": (29, Units.W, to_signed), + "Yield total": (pack_u16(34, 35), Total(Units.KWH), div10), + "Yield today": (36, Units.KWH, div10), + "Battery Remaining Capacity": (80, Units.PERCENT), + "Battery Temperature": (82, Units.C), + "Battery Surplus Energy": (83, Units.KWH, div10), + } + + # pylint: enable=duplicate-code diff --git a/solax/inverters/x1_boost_g4.py b/solax/inverters/x1_boost_g4.py new file mode 100644 index 0000000..ace3edf --- /dev/null +++ b/solax/inverters/x1_boost_g4.py @@ -0,0 +1,89 @@ +import voluptuous as vol + +from solax import utils +from solax.inverter import Inverter, InverterHttpClient, Method, ResponseParser +from solax.units import Total, Units +from solax.utils import div10, div100, pack_u16, to_signed, to_signed32 + + +class X1BoostG4(Inverter): + # pylint: disable=duplicate-code + _schema = vol.Schema( + { + vol.Required("type"): vol.All(int, 18), + vol.Required( + "sn", + ): str, + vol.Required("ver"): str, + vol.Required("Data"): vol.Schema( + vol.All( + [vol.Coerce(float)], + vol.Length(min=100, max=100), + ) + ), + + vol.Required("Information"): vol.Schema(vol.All(vol.Length(min=9, max=10))), + }, + extra=vol.REMOVE_EXTRA, + ) + + @classmethod + def _build(cls, host, port, pwd="", params_in_query=True): + + cls.url = utils.to_url(host, port) + cls.pwd = pwd + http_client = InverterHttpClient(cls.url, Method.POST, cls.pwd).with_default_data() + + response_parser = ResponseParser(cls._schema, cls.response_decoder()) + return cls(http_client, response_parser) + + @classmethod + def build_all_variants(cls, host, port, pwd=""): + versions = [cls._build(host, port, pwd)] + return versions + + @classmethod + def _decode_run_mode(cls, run_mode): + return { + 0: "Waiting", + 1: "Checking", + 2: "Normal", + 3: "Fault", + 4: "Permanent Fault", + 5: "Updating", + 6: "EpsCheck", + 7: "Eps", + }.get(run_mode) + + @classmethod + def response_decoder(cls): + return { + "Grid Voltage": (0, Units.V, div10), + "Grid Current": (1, Units.A, div10), + "Grid Frequency": (2, Units.HZ, div100), + "Grid Power": (3, Units.W, to_signed), + "PV1 Voltage": (4, Units.V, div10), + "PV2 Voltage": (5, Units.V, div10), + "PV3 Voltage": (6, Units.V, div10), + "PV4 Voltage": (7, Units.V, div10), + "Pv1 Current": (8, Units.A, div10), + "Pv2 Current": (9, Units.A, div10), + "Run Mode": (10, Units.NONE, X1BoostG4._decode_run_mode), + "Pv3 Current": (11, Units.A, div10), + "Pv4 Current": (12, Units.A, div10), + "PV1 Power": (13, Units.W, to_signed), + "PV2 Power": (14, Units.W, to_signed), + "PV3 Power": (15, Units.W, to_signed), + "PV4 Power": (16, Units.W, to_signed), + "Yield Total": (pack_u16(19, 20), Total(Units.KWH), div10), + "Yield Today": (21, Units.KWH, div10), + "Rate Power": (22, Units.KWH, to_signed), + "Radiator Temperature": (23, Units.C, to_signed), + "Invert temperature": (24, Units.C, to_signed), + "Feed in Power": (pack_u16(72, 73), Units.W, to_signed32), + "Feed in Energy ": (pack_u16(74, 75), Units.KWH, div100), + "Consume Energy": (pack_u16(76, 77), Units.KWH, div100), + + } + + # pylint: enable=duplicate-code diff --git a/solax/inverters/x1_hybrid_g2.py b/solax/inverters/x1_hybrid_g2.py new file mode 100644 index 0000000..7833f15 --- /dev/null +++ b/solax/inverters/x1_hybrid_g2.py @@ -0,0 +1,112 @@ +import voluptuous as vol + +from solax import utils +from solax.inverter import Inverter, InverterHttpClient, Method, ResponseParser +from solax.units import Total, Units +from solax.utils import div10, div100, pack_u16, to_signed, to_signed32 + + +class X1HybridG2(Inverter): + # pylint: disable=duplicate-code + _schema = vol.Schema( + { + vol.Required("type"): vol.All(int, 19), + vol.Required( + "sn", + ): str, + vol.Required("ver"): str, + vol.Required("Data"): vol.Schema( + vol.All( + [vol.Coerce(float)], + vol.Length(min=290, max=300), + ) + ), + + vol.Required("Information"): vol.Schema(vol.All(vol.Length(min=9, max=10))), + }, + extra=vol.REMOVE_EXTRA, + ) + + @classmethod + def _build(cls, host, port, pwd="", params_in_query=True): + cls.url = utils.to_url(host, port) + cls.pwd = pwd + http_client = InverterHttpClient(cls.url, Method.POST, cls.pwd).with_default_data() + + response_parser = ResponseParser(cls._schema, cls.response_decoder()) + return cls(http_client, response_parser) + + @classmethod + def build_all_variants(cls, host, port, pwd=""): + versions = [cls._build(host, port, pwd)] + return versions + + @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", + 11: "Gen Check Mode", + 12: "Gen Run Mode", + 13: "RSD Standby", + }.get(run_mode) + + @classmethod + def response_decoder(cls): + return { + "Grid Voltage": (4, Units.V, div10), + "Grid Current": (5, Units.A, div10), + "Grid Power": (6, Units.W, to_signed), + "Grid Frequency": (7, Units.HZ, div100), + "Run Mode": (10, Units.NONE, X1HybridG2._decode_run_mode), + "PV1 Voltage": (11, Units.V, div10), + "PV2 Voltage": (12, Units.V, div10), + "PV3 Voltage": (13, Units.V, div10), + "Pv1 Current": (15, Units.A, div10), + "Pv2 Current": (16, Units.A, div10), + "Pv3 Current": (17, Units.A, div10), + "PV1 Power": (19, Units.W, to_signed), + "PV2 Power": (20, Units.W, to_signed), + "PV3 Power": (21, Units.W, to_signed), + "EPS Apparent Power": (23, Units.W, to_signed), + "EPS Voltage": (24, Units.V, div10), + "EPS Current": (25, Units.A, div10), + "EPS Frequency": (26, Units.HZ, div100), + "EPS Active Power": (27, Units.W, to_signed), + "Feed in Power": (pack_u16(28, 29), Units.W, to_signed32), + "Selfuse Power": (30, Units.W, to_signed), + "Active Power": (31, Units.W, to_signed), + "ReactivePower": (32, Units.W, to_signed), + "Feed in Energy ": (pack_u16(33, 34), Units.KWH, div10), + "Consume Energy": (pack_u16(35, 36), Units.KWH, div10), + "Feedin Energy Today": (pack_u16(37, 38), Units.KWH, div10), + "Consume Energy Today": (pack_u16(39, 40), Units.KWH, div10), + "Yield Total": (pack_u16(41, 42), Total(Units.KWH), div10), + "Yield Today": (43, Units.KWH, div10), + "Solar Yield Total": (pack_u16(44, 45), Units.KWH, div10), + "Solar Yield Today": (46, Units.KWH, div10), + "Eps Yield Total": (pack_u16(47, 48), Units.KWH, div10), + "Eps Yield Today": (49, Units.KWH, div10), + "BatCharge Yield Total": (pack_u16(50, 51), Units.KWH, div10), + "OutputEnergy Charge Today": (52, Units.W, div10), + "InputEnergy Charge Today": (53, Units.W, div10), + "Green Solar Yield Total": (pack_u16(54, 55), Units.KWH, div10), + "Green Solar Yield Today": (56, Units.W, div10), + "Battery Voltage": (89, Units.V, div100), + "Battery Current": (90, Units.A, div100), + "Battery Power": (91, Units.W, to_signed), + "Battery Temperature": (92, Units.C, to_signed), + "Battery Capacity": (93, Units.PERCENT), + "Battery OutputEnergy": (pack_u16(94, 95), Units.KWH, div10), + "Battery SurplusEnergy": (99, Units.KWH, div10), + } + # pylint: enable=duplicate-code diff --git a/solax/inverters/x1_hybrid_gen4.py b/solax/inverters/x1_hybrid_gen4.py index 0962586..3cbe5be 100644 --- a/solax/inverters/x1_hybrid_gen4.py +++ b/solax/inverters/x1_hybrid_gen4.py @@ -3,7 +3,7 @@ from solax import utils from solax.inverter import Inverter, InverterHttpClient, Method, ResponseParser from solax.units import Total, Units -from solax.utils import div10, div100, pack_u16, to_signed +from solax.utils import div10, div100, pack_u16, to_signed, to_signed32 class X1HybridGen4(Inverter): @@ -28,8 +28,9 @@ class X1HybridGen4(Inverter): @classmethod def _build(cls, host, port, pwd="", params_in_query=True): - url = utils.to_url(host, port) - http_client = InverterHttpClient(url, Method.POST, pwd).with_default_data() + cls.url = utils.to_url(host, port) + cls.pwd = pwd + http_client = InverterHttpClient(cls.url, Method.POST, cls.pwd).with_default_data() response_parser = ResponseParser(cls._schema, cls.response_decoder()) return cls(http_client, response_parser) @@ -39,27 +40,57 @@ def build_all_variants(cls, host, port, pwd=""): versions = [cls._build(host, port, pwd)] return versions + @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 { - "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), + "Grid Voltage": (0, Units.V, div10), + "Grid Current": (1, Units.A, div10), + "Grid Power": (2, Units.W, to_signed), + "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, to_signed), + "PV2 Power": (9, Units.W, to_signed), + "Run Mode": (10, Units.NONE, X1HybridGen4._decode_run_mode), + "Yield Total": (pack_u16(11, 12), Total(Units.KWH), div10), + "Yield Today": (13, Units.KWH, div10), + "Battery Voltage": (14, Units.V, div100), + "Battery Current": (15, Units.A, div100), + "Battery Power": (16, Units.W, to_signed), + "Battery Temperature": (17, Units.C, to_signed), + "Battery Capacity": (18, Units.PERCENT), + "Battery Discharge Yield Total": (pack_u16(19, 20), Units.KWH, div10), + "Battery Charge Yield Total": (pack_u16(21, 22), Units.KWH, div10), + "Battery SurplusEnergy": (23, Units.KWH, div10), + "Power Factor": (24, Units.NONE, div100), + "Radiator Temperature": (26, Units.C, to_signed), + "Bus Voltage": (27, Units.V, div10), + "EPS Power Active": (28, Units.W, to_signed), + "EPS Voltage": (29, Units.V, div10), + "EPS Current": (30, Units.A, div10), + "EPS Frequency": (31, Units.HZ, div100), + "Feed in Power": (pack_u16(32, 33), Units.W, to_signed32), + "Feed in Energy ": (pack_u16(34, 35), Units.KWH, div100), + "Consume Energy": (pack_u16(36, 37), Units.KWH, div100), + "Selfuse Power": (38, Units.W, to_signed), + "Invert Temperature": (39, Units.C, to_signed), + } diff --git a/solax/inverters/x1_hybrid_lv.py b/solax/inverters/x1_hybrid_lv.py new file mode 100644 index 0000000..72fea1c --- /dev/null +++ b/solax/inverters/x1_hybrid_lv.py @@ -0,0 +1,103 @@ +import voluptuous as vol + +from solax import utils +from solax.inverter import Inverter, InverterHttpClient, Method, ResponseParser +from solax.units import Total, Units +from solax.utils import div10, div100, pack_u16, to_signed, to_signed32 + + +class X1HybridLv(Inverter): + # pylint: disable=duplicate-code + _schema = vol.Schema( + { + vol.Required("type"): vol.All(int, 102), + 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 _build(cls, host, port, pwd="", params_in_query=True): + cls.url = utils.to_url(host, port) + cls.pwd = pwd + http_client = InverterHttpClient(cls.url, Method.POST, cls.pwd).with_default_data() + + response_parser = ResponseParser(cls._schema, cls.response_decoder()) + return cls(http_client, response_parser) + + @classmethod + def build_all_variants(cls, host, port, pwd=""): + versions = [cls._build(host, port, pwd)] + return versions + + @classmethod + def _decode_run_mode(cls, run_mode): + return { + 0: "Waiting", + 1: "Checking", + 2: "Normal", + 3: "Fault", + 4: "Permanent Fault", + 5: "Updating", + 6: "EpsCheck", + 7: "Eps", + 8: "Self Test", + 9: "Idle", + 10: "Standby", + }.get(run_mode) + + @classmethod + def response_decoder(cls): + return { + "Grid Voltage": (0, Units.V, div10), + "Grid Current": (1, Units.A, div10), + "Grid Power": (2, Units.W, to_signed), + "Grid Frequency": (3, Units.HZ, div100), + "PV1 Voltage": (5, Units.V, div10), + "Pv1 Current": (6, Units.A, div10), + "PV2 Voltage": (7, Units.V, div10), + "Pv2 Current": (8, Units.A, div10), + "PV1 Power": (9, Units.W, to_signed), + "PV2 Power": (10, Units.W, to_signed), + "Run Mode": (12, Units.NONE, X1HybridLv._decode_run_mode), + "Yield Total": (pack_u16(14, 15), Total(Units.KWH), div10), + "Consume Total": (pack_u16(16, 17), Total(Units.KWH), div10), + "Yield Today": (18, Units.KWH, div10), + "Consume Today": (19, Units.KWH, div10), + "Battery Voltage": (20, Units.V, div10), + "Battery Current": (21, Units.A, div10), + "Battery Power": (22, Units.W, to_signed), + "Battery Charge Total": (pack_u16(23, 24), Total(Units.KWH), div10), + "Battery Discharge Total": (pack_u16(25, 26), Total(Units.KWH), div10), + "Battery Charge Today": (27, Total(Units.KWH), div10), + "Battery Discharge Today": (28, Total(Units.KWH), div10), + "Feed in Power": (pack_u16(29, 30), Units.W, to_signed32), + "Feed in Energy Total": (pack_u16(31, 32), Units.KWH, div10), + "Consume Energy Total": (pack_u16(33, 34), Units.KWH, div10), + "Feed in Energy Today": (pack_u16(35, 36), Units.KWH, div10), + "Consume Energy Today": (pack_u16(37, 38), Units.KWH, div10), + "Invert temperature": (39, Units.C, div10), + "Radiator Temperature": (40, Units.C, div10), + "EPS Voltage": (41, Units.V, div10), + "EPS Current": (42, Units.A, div10), + "EPS Power": (43, Units.W, div10), + "EPS Frequency": (44, Units.HZ, div100), + "EPS Today": (45, Units.KWH, div10), + "EPS Total": (pack_u16(46, 47), Units.KWH, div10), + "PV Today": (51, Units.KWH, div10), + "PV Total": (pack_u16(52, 53), Units.KWH, div10), + + } + + # pylint: enable=duplicate-code diff --git a/solax/inverters/x1_ies.py b/solax/inverters/x1_ies.py new file mode 100644 index 0000000..900e7a2 --- /dev/null +++ b/solax/inverters/x1_ies.py @@ -0,0 +1,104 @@ +import voluptuous as vol + +from solax import utils +from solax.inverter import Inverter, InverterHttpClient, Method, ResponseParser +from solax.units import Total, Units +from solax.utils import div10, div100, pack_u16, to_signed, to_signed32 + + +class X1Ies(Inverter): + # pylint: disable=duplicate-code + _schema = vol.Schema( + { + vol.Required("type"): vol.All(int, 23), + 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=9, max=10))), + }, + extra=vol.REMOVE_EXTRA, + ) + pwd: str = '' + url: str = '' + + @classmethod + def _build(cls, host, port, pwd="", params_in_query=True): + cls.pwd = pwd + cls.url = utils.to_url(host, port) + http_client = InverterHttpClient(cls.url, Method.POST, pwd).with_default_data() + + response_parser = ResponseParser(cls._schema, cls.response_decoder()) + return cls(http_client, response_parser) + + @classmethod + def build_all_variants(cls, host, port, pwd=""): + versions = [cls._build(host, port, pwd)] + return versions + + @classmethod + def _decode_run_mode(cls, run_mode): + return { + 0: "Waiting", + 1: "Checking", + 2: "Normal", + 3: "Fault", + 4: "Permanent Fault", + 5: "Updating", + 6: "EpsCheck", + 7: "Eps", + 8: "Self Test", + 9: "Idle", + 10: "Standby", + 11: "Init", + }.get(run_mode) + + @classmethod + def response_decoder(cls): + return { + "Grid Voltage": (0, Units.V, div10), + "Grid Current": (1, Units.A, div10), + "Grid Power": (2, Units.W, to_signed), + "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, to_signed), + "PV2 Power": (9, Units.W, to_signed), + "Run Mode": (10, Units.NONE, X1Ies._decode_run_mode), + "Yield Total": (pack_u16(11, 12), Total(Units.KWH), div10), + "Yield Today": (13, Units.KWH, div10), + "Battery Voltage": (14, Units.V, div100), + "Battery Current": (15, Units.A, div100), + "Battery Power": (16, Units.W, to_signed), + "Battery Temperature": (17, Units.C, to_signed), + "Battery Capacity": (18, Units.PERCENT), + "Battery Discharge Total": (pack_u16(19, 20), Total(Units.KWH), div10), + "Battery Charge Total": (pack_u16(21, 22), Total(Units.KWH), div10), + "Battery Surplus Energy": (23, Units.W, div10), + "Radiator Temperature": (26, Units.C, to_signed), + "EPS Power": (28, Units.W, to_signed), + "EPS Voltage": (29, Units.V, div10), + "EPS Current": (30, Units.A, div10), + "EPS Frequency": (31, Units.HZ, div100), + "Feed in Power": (pack_u16(32, 33), Units.W, to_signed32), + "Feed in Energy Total": (pack_u16(34, 35), Units.KWH, div100), + "Consume Total": (pack_u16(36, 37), Total(Units.KWH), div100), + "Selfuse Power": (38, Units.W, to_signed), + "Invert temperature": (39, Units.C, to_signed), + "Feed in Energy Today": (pack_u16(78, 79), Units.KWH, div100), + "Consume Energy Today": (pack_u16(80, 81), Units.KWH, div100), + "EPS Today": (82, Units.KWH, div10), + "EPS Total": (pack_u16(83, 84), Units.KWH, div10), + + } + + # pylint: enable=duplicate-code diff --git a/solax/inverters/x1_mini_g3.py b/solax/inverters/x1_mini_g3.py new file mode 100644 index 0000000..bc62c5a --- /dev/null +++ b/solax/inverters/x1_mini_g3.py @@ -0,0 +1,108 @@ +import voluptuous as vol + +from solax import utils +from solax.inverter import Inverter, InverterHttpClient, Method, ResponseParser +from solax.units import Total, Units +from solax.utils import div10, div100, pack_u16, to_signed, to_signed32 + + +class X1MiniG3(Inverter): + # pylint: disable=duplicate-code + _schema = vol.Schema( + { + vol.Required("type"): vol.All(int, 4), + vol.Required( + "sn", + ): str, + vol.Required("ver"): str, + vol.Required("Data"): vol.Schema( + vol.All( + [vol.Coerce(float)], + vol.Length(min=100, max=100), + ) + ), + + vol.Required("Information"): vol.Schema(vol.All(vol.Length(min=9, max=10))), + }, + extra=vol.REMOVE_EXTRA, + ) + + @classmethod + def _build(cls, host, port, pwd="", params_in_query=True): + cls.pwd = pwd + cls.url = utils.to_url(host, port) + http_client = InverterHttpClient(cls.url, Method.POST, cls.pwd).with_default_data() + response_parser = ResponseParser(cls._schema, cls.response_decoder()) + return cls(http_client, response_parser) + + @classmethod + def build_all_variants(cls, host, port, pwd=""): + versions = [cls._build(host, port, pwd)] + return versions + + @classmethod + def _decode_run_mode(cls, run_mode): + return { + 0: "Waiting", + 1: "Checking", + 2: "Normal", + 3: "Fault", + 4: "Permanent Fault", + 5: "Updating", + }.get(run_mode) + + @classmethod + def _decode_switch_statue(cls, switch_statue): + return { + 0: "Open", + 1: "Close", + + }.get(switch_statue) + + @classmethod + def _decode_signal_strength(cls, signal_strength): + return { + 0: "No signal", + 1: "Very weak signal", + 2: "Weak signal", + 3: "Moderate signal", + 4: "Strong signal", + 5: "Very strong signal", + + }.get(signal_strength) + + @classmethod + def response_decoder(cls): + return { + "Grid Voltage": (0, Units.V, div10), + "Grid Current": (1, Units.A, div10), + "Grid Power": (2, Units.W, to_signed), + "PV1 Voltage": (3, Units.V, div10), + "PV2 Voltage": (4, Units.V, div10), + "Pv1 Current": (5, Units.A, div10), + "Pv2 Current": (6, Units.A, div10), + "PV1 Power": (7, Units.W, to_signed), + "PV2 Power": (8, Units.W, to_signed), + "Grid Frequency": (9, Units.HZ, div100), + "Run Mode": (10, Units.NONE, X1MiniG3._decode_run_mode), + "Yield Total": (pack_u16(11, 12), Total(Units.KWH), div10), + "Yield Today": (13, Units.KWH, div10), + "Load1 Power": (26, Units.W, to_signed), + "Load1 Yield": (pack_u16(27, 28), Total(Units.KWH), div100), + "Load2 Power": (32, Units.W, to_signed), + "Load2 Yield": (pack_u16(33, 34), Total(Units.KWH), div100), + "Feed in Power": (pack_u16(48, 49), Units.W, to_signed32), + "Feed in Energy ": (pack_u16(50, 51), Units.KWH, div100), + "Consume Energy": (pack_u16(52, 53), Units.KWH, div100), + "Invert temperature": (55, Units.C, to_signed), + + } + + # pylint: enable=duplicate-code + + +data = [2237, 78, 1745, 3627, 0, 48, 0, 1755, 0, 4999, 2, 120, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 4750, 5015, 2050, 2530, 4750, 5010, 1955, 30, 2760, 1, 0, 5150, 10, 0, 0, 0, 0, 0, 56, 0, 61, 0, 0, 32, 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] +print(data[34]) diff --git a/solax/inverters/x1_mini_g4.py b/solax/inverters/x1_mini_g4.py new file mode 100644 index 0000000..e2cc8ca --- /dev/null +++ b/solax/inverters/x1_mini_g4.py @@ -0,0 +1,91 @@ +import voluptuous as vol + +from solax import utils +from solax.inverter import Inverter, InverterHttpClient, Method, ResponseParser +from solax.units import Total, Units +from solax.utils import div10, div100, pack_u16, to_signed, to_signed32 + + +class X1MiniG4(Inverter): + # pylint: disable=duplicate-code + _schema = vol.Schema( + { + vol.Required("type"): vol.All(int, 22), + vol.Required( + "sn", + ): str, + vol.Required("ver"): str, + vol.Required("Data"): vol.Schema( + vol.All( + [vol.Coerce(float)], + vol.Length(min=100, max=100), + ) + ), + + vol.Required("Information"): vol.Schema(vol.All(vol.Length(min=9, max=10))), + }, + extra=vol.REMOVE_EXTRA, + ) + + @classmethod + def _build(cls, host, port, pwd="", params_in_query=True): + cls.pwd = pwd + cls.url = utils.to_url(host, port) + http_client = InverterHttpClient(cls.url, Method.POST, cls.pwd).with_default_data() + response_parser = ResponseParser(cls._schema, cls.response_decoder()) + return cls(http_client, response_parser) + + @classmethod + def build_all_variants(cls, host, port, pwd=""): + versions = [cls._build(host, port, pwd)] + return versions + + @classmethod + def _decode_run_mode(cls, run_mode): + return { + 0: "Waiting", + 1: "Checking", + 2: "Normal", + 3: "Fault", + 4: "Permanent Fault", + 5: "Updating", + 6: "EpsCheck", + 7: "Eps", + }.get(run_mode) + + + + @classmethod + def response_decoder(cls): + return { + "Grid Voltage": (0, Units.V, div10), + "Grid Current": (1, Units.A, div10), + "Grid Frequency": (2, Units.HZ, div100), + "Grid Power": (3, Units.W, to_signed), + "PV1 Voltage": (4, Units.V, div10), + "PV2 Voltage": (5, Units.V, div10), + "PV3 Voltage": (6, Units.V, div10), + "PV4 Voltage": (7, Units.V, div10), + "Pv1 Current": (8, Units.A, div10), + "Pv2 Current": (9, Units.A, div10), + "Run Mode": (10, Units.NONE, X1MiniG4._decode_run_mode), + "Pv3 Current": (11, Units.A, div10), + "Pv4 Current": (12, Units.A, div10), + "PV1 Power": (13, Units.W, to_signed), + "PV2 Power": (14, Units.W, to_signed), + "PV3 Power": (15, Units.W, to_signed), + "PV4 Power": (16, Units.W, to_signed), + "Yield Total": (pack_u16(19, 20), Total(Units.KWH), div10), + "Yield Today": (21, Units.KWH, div10), + "Rate Power": (22, Units.KWH, to_signed), + "Radiator Temperature": (23, Units.C, to_signed), + "Invert temperature": (24, Units.C, to_signed), + "Feed in Power": (pack_u16(72, 73), Units.W, to_signed32), + "Feed in Energy ": (pack_u16(74, 75), Units.KWH, div100), + "Consume Energy": (pack_u16(76, 77), Units.KWH, div100), + + + } + + # pylint: enable=duplicate-code + diff --git a/solax/inverters/x3_fth.py b/solax/inverters/x3_fth.py new file mode 100644 index 0000000..3a3d997 --- /dev/null +++ b/solax/inverters/x3_fth.py @@ -0,0 +1,89 @@ +import voluptuous as vol + +from solax.inverter import Inverter +from solax.units import Total, Units +from solax.utils import div10, div100, pack_u16, to_signed, to_signed32, twoway_div10 + + +class X3Forth(Inverter): + # pylint: disable=duplicate-code + _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=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: "Init", + 1: "Idle", + 2: "Start", + 3: "Run", + 4: "Fault Fault", + 5: "Update" + }.get(run_mode) + + @classmethod + def response_decoder(cls): + return { + "Grid Voltage Line AB": (24, Units.V, div10), + "Grid Voltage Line BC": (25, Units.V, div10), + "Grid Voltage Line CA": (26, Units.V, div10), + "Grid A Voltage": (27, Units.V, div10), + "Grid B Voltage": (28, Units.V, div10), + "Grid C Voltage": (29, Units.V, div10), + "Grid A Current": (30, Units.A, div10), + "Grid B Current": (31, Units.A, div10), + "Grid C Current": (32, Units.A, div10), + "Grid A Power": (pack_u16(33, 34), Units.W, to_signed), + "Grid B Power": (pack_u16(35, 36), Units.W, to_signed), + "Grid C Power": (pack_u16(37, 38), Units.W, to_signed), + "GridFrequency": (39, Units.HZ, div100), + "Yield_Total": (pack_u16(46, 47), Units.KWH, div10), + "Yield_Today": (48, Units.KWH, div10), + "Run Mode": (56, Units.NONE, X3Forth._decode_run_mode), + "PV1 Voltage": (87, Units.V, div10), + "PV1 Current": (88, Units.A, div10), + "PV2 Voltage": (89, Units.V, div10), + "PV2 Current": (90, Units.A, div10), + "PV3 Voltage": (91, Units.V, div10), + "PV3 Current": (92, Units.A, div10), + "PV4 Voltage": (93, Units.V, div10), + "PV4 Current": (94, Units.A, div10), + "PV5 Voltage": (95, Units.V, div10), + "PV5 Current": (96, Units.A, div10), + "PV6 Voltage": (97, Units.V, div10), + "PV6 Current": (98, Units.A, div10), + "PV7 Voltage": (99, Units.V, div10), + "PV7 Current": (100, Units.A, div10), + "PV8 Voltage": (101, Units.V, div10), + "PV8 Current": (102, Units.A, div10), + "PV9 Voltage": (103, Units.V, div10), + "PV9 Current": (104, Units.A, div10), + "PV10 Voltage": (105, Units.V, div10), + "PV10 Current": (106, Units.A, div10), + "PV11 Voltage": (107, Units.V, div10), + "PV11 Current": (108, Units.A, div10), + "PV12 Voltage": (109, Units.V, div10), + "PV12 Current": (110, Units.A, div10), + + } + + # pylint: enable=duplicate-code diff --git a/solax/inverters/x3_hybrid_g4.py b/solax/inverters/x3_hybrid_g4.py index ebe2cf7..a8f1295 100644 --- a/solax/inverters/x3_hybrid_g4.py +++ b/solax/inverters/x3_hybrid_g4.py @@ -1,6 +1,7 @@ import voluptuous as vol from solax.inverter import Inverter + from solax.units import Total, Units from solax.utils import div10, div100, pack_u16, to_signed, to_signed32, twoway_div10 @@ -68,27 +69,38 @@ def response_decoder(cls): "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), - "Run mode text": (19, Units.NONE, X3HybridG4._decode_run_mode), + "Run Mode": (19, Units.NONE, X3HybridG4._decode_run_mode), "EPS 1 Voltage": (23, Units.W, div10), "EPS 2 Voltage": (24, Units.W, div10), "EPS 3 Voltage": (25, Units.W, div10), - "EPS 1 Current": (26, Units.W, twoway_div10), - "EPS 2 Current": (27, Units.W, twoway_div10), - "EPS 3 Current": (28, Units.W, twoway_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), - "Feed-in Power ": (pack_u16(34, 35), Units.W, to_signed32), - "Battery Power": (41, Units.W, to_signed), - "Radiator Temperature": (54, Units.C, to_signed), - "Yield total": (pack_u16(68, 69), Total(Units.KWH), div10), - "Yield today": (70, Units.KWH, div10), - "Feed-in Energy": (pack_u16(86, 87), Total(Units.KWH), div100), - "Consumed Energy": (pack_u16(88, 89), Total(Units.KWH), div100), - "Battery Remaining Capacity": (103, Units.PERCENT), - "Battery Temperature": (105, Units.C, to_signed), - "Battery Voltage": (pack_u16(169, 170), Units.V, div100), + "EPS Frequency": (32, Units.HZ, to_signed), + "Invert Temperature": (46, Units.C, to_signed), + "Grid Output Yield Total": (pack_u16(68, 69), Units.KWH, div10), + "Grid Output Yield Today": (70, Units.KWH, div10), + "Grid Input Yield Total": (pack_u16(71, 72), Units.KWH, div10), + "Grid Input Yield Today": (73, Units.KWH, div10), + "Battery Discharge Total": (pack_u16(74, 75), Total(Units.KWH), div10), + "Battery Charge Total": (pack_u16(76, 77), Total(Units.KWH), div10), + "Output Energy Charge Today": (78, Units.KWH, div10), + "Input Energy Charge Today": (79, Units.KWH, div10), + "Pv Yield Total": (pack_u16(80, 81), Total(Units.KWH), div10), + "Solar Energy Today": (82, Units.KWH, div10), + "Eps Yield Total": (pack_u16(83, 84), Total(Units.KWH), div10), + "Eps Yield Today": (85, Units.KWH, div10), + "Feed in Energy Total": (pack_u16(86, 87), Units.KWH, div100), + "Consume Total": (pack_u16(88, 89), Total(Units.KWH), div100), + "Feed in Energy Today": (pack_u16(90, 91), Units.KWH, div100), + "Consume Today": (pack_u16(92, 93), Total(Units.KWH), div100), + "Battery Capacity": (103, Units.PERCENT), + "Battery Temperature": (105, Units.C, to_signed), + "Battery Surplus Energy": (106, Units.KWH, div10), + } # pylint: enable=duplicate-code diff --git a/solax/inverters/x3_ies.py b/solax/inverters/x3_ies.py new file mode 100644 index 0000000..5a6683c --- /dev/null +++ b/solax/inverters/x3_ies.py @@ -0,0 +1,122 @@ +import voluptuous as vol + +from solax import utils +from solax.inverter import Inverter, InverterHttpClient, Method, ResponseParser +from solax.units import Total, Units +from solax.utils import div10, div100, pack_u16, to_signed, to_signed32 + + +class X3Ies(Inverter): + # pylint: disable=duplicate-code + _schema = vol.Schema( + { + vol.Required("type"): vol.All(int, 24), + 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=9, max=10))), + }, + extra=vol.REMOVE_EXTRA, + ) + + @classmethod + def _build(cls, host, port, pwd="", params_in_query=True): + cls.pwd = pwd + cls.url = utils.to_url(host, port) + http_client = InverterHttpClient(cls.url, Method.POST, cls.pwd).with_default_data() + response_parser = ResponseParser(cls._schema, cls.response_decoder()) + return cls(http_client, response_parser) + + @classmethod + def build_all_variants(cls, host, port, pwd=""): + versions = [cls._build(host, port, pwd)] + return versions + + @classmethod + def _decode_run_mode(cls, run_mode): + return { + 0: "Waiting", + 1: "Checking", + 2: "Normal", + 3: "Fault", + 4: "Permanent Fault", + 5: "Updating", + 6: "EpsCheck", + 7: "Eps", + 8: "Self Test", + 9: "Idle", + 10: "Standby", + 20: "VPP", + 21: "TOU-Self use", + 22: "TOU-Charging", + 23: "TOU-Discharging", + 24: "TOU-Battery off", + 25: "TOU-Peak Shaving", + }.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, div10), + "Grid 2 Current": (4, Units.A, div10), + "Grid 3 Current": (5, Units.A, 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), + "Grid Power Total": (9, Units.W, to_signed), + "PV1 Voltage": (10, Units.V, div10), + "PV2 Voltage": (11, Units.V, div10), + "Pv1 Current": (12, Units.A, div10), + "Pv2 Current": (13, Units.A, div10), + "PV1 Power": (14, Units.W, to_signed), + "PV2 Power": (15, Units.W, to_signed), + "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, X3Ies._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, div10), + "EPS 2 Current": (27, Units.A, div10), + "EPS 3 Current": (28, Units.A, 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), + "EPS Frequency": (32, Units.HZ, div100), + "Battery Voltage": (39, Units.V, div100), + "Battery Current": (40, Units.A, div100), + "Battery Power": (41, Units.W, to_signed), + "Invert temperature": (46, Units.C, to_signed), + "Selfuse Power": (47, Units.W, to_signed), + "Radiator Temperature": (54, Units.C, to_signed), + "Yield Output Today": (70, Units.KWH, div10), + "Yield Input Total": (pack_u16(71, 72), Total(Units.KWH), div10), + "Yield Input Today": (73, Units.KWH, div10), + "Battery Discharge Total": (pack_u16(74, 75), Total(Units.KWH), div10), + "Battery Charge Total": (pack_u16(76, 77), Total(Units.KWH), div10), + "OutputEnergy Charge Today": (78, Units.KWH, div10), + "InputEnergy Charge Today": (79, Units.KWH, div10), + "Pv Yield Total": (pack_u16(80, 81), Total(Units.KWH), div10), + "Solar Energy Today": (82, Units.KWH, div10), + "Eps Yield Total": (pack_u16(83, 84), Total(Units.KWH), div10), + "Eps Yield Today": (85, Units.KWH, div10), + "Feed in Energy Total": (pack_u16(86, 87), Total(Units.KWH), div100), + "Consume Energy Total": (pack_u16(88, 89), Total(Units.KWH), div100), + "Feed In Energy Today": (pack_u16(90, 91), Total(Units.KWH), div100), + "Consume Energy Today": (pack_u16(92, 93), Total(Units.KWH), div100), + + } + + # pylint: enable=duplicate-code diff --git a/solax/inverters/x3_mga_g2.py b/solax/inverters/x3_mga_g2.py new file mode 100644 index 0000000..3a3d997 --- /dev/null +++ b/solax/inverters/x3_mga_g2.py @@ -0,0 +1,89 @@ +import voluptuous as vol + +from solax.inverter import Inverter +from solax.units import Total, Units +from solax.utils import div10, div100, pack_u16, to_signed, to_signed32, twoway_div10 + + +class X3Forth(Inverter): + # pylint: disable=duplicate-code + _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=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: "Init", + 1: "Idle", + 2: "Start", + 3: "Run", + 4: "Fault Fault", + 5: "Update" + }.get(run_mode) + + @classmethod + def response_decoder(cls): + return { + "Grid Voltage Line AB": (24, Units.V, div10), + "Grid Voltage Line BC": (25, Units.V, div10), + "Grid Voltage Line CA": (26, Units.V, div10), + "Grid A Voltage": (27, Units.V, div10), + "Grid B Voltage": (28, Units.V, div10), + "Grid C Voltage": (29, Units.V, div10), + "Grid A Current": (30, Units.A, div10), + "Grid B Current": (31, Units.A, div10), + "Grid C Current": (32, Units.A, div10), + "Grid A Power": (pack_u16(33, 34), Units.W, to_signed), + "Grid B Power": (pack_u16(35, 36), Units.W, to_signed), + "Grid C Power": (pack_u16(37, 38), Units.W, to_signed), + "GridFrequency": (39, Units.HZ, div100), + "Yield_Total": (pack_u16(46, 47), Units.KWH, div10), + "Yield_Today": (48, Units.KWH, div10), + "Run Mode": (56, Units.NONE, X3Forth._decode_run_mode), + "PV1 Voltage": (87, Units.V, div10), + "PV1 Current": (88, Units.A, div10), + "PV2 Voltage": (89, Units.V, div10), + "PV2 Current": (90, Units.A, div10), + "PV3 Voltage": (91, Units.V, div10), + "PV3 Current": (92, Units.A, div10), + "PV4 Voltage": (93, Units.V, div10), + "PV4 Current": (94, Units.A, div10), + "PV5 Voltage": (95, Units.V, div10), + "PV5 Current": (96, Units.A, div10), + "PV6 Voltage": (97, Units.V, div10), + "PV6 Current": (98, Units.A, div10), + "PV7 Voltage": (99, Units.V, div10), + "PV7 Current": (100, Units.A, div10), + "PV8 Voltage": (101, Units.V, div10), + "PV8 Current": (102, Units.A, div10), + "PV9 Voltage": (103, Units.V, div10), + "PV9 Current": (104, Units.A, div10), + "PV10 Voltage": (105, Units.V, div10), + "PV10 Current": (106, Units.A, div10), + "PV11 Voltage": (107, Units.V, div10), + "PV11 Current": (108, Units.A, div10), + "PV12 Voltage": (109, Units.V, div10), + "PV12 Current": (110, Units.A, div10), + + } + + # pylint: enable=duplicate-code diff --git a/solax/inverters/x3_mic_pro_g2.py b/solax/inverters/x3_mic_pro_g2.py index 76df85f..b3247ba 100644 --- a/solax/inverters/x3_mic_pro_g2.py +++ b/solax/inverters/x3_mic_pro_g2.py @@ -70,6 +70,9 @@ def response_decoder(cls): "Run Mode": (21, Units.NONE, X3MicProG2._decode_run_mode), "Total Yield": (pack_u16(22, 23), Total(Units.KWH), div10), "Daily Yield": (24, Units.KWH, div10), + "Rate Power": (25, Units.W, to_signed), + "Radiator Temperature": (26, Units.C, to_signed), + "Invert_temprature": (27, Units.C, to_signed), "Feed-in Power ": (pack_u16(72, 73), Units.W, to_signed32), "Total Feed-in Energy": (pack_u16(74, 75), Total(Units.KWH), div100), "Total Consumption": (pack_u16(76, 77), Total(Units.KWH), div100), diff --git a/solax/inverters/x3_ultra.py b/solax/inverters/x3_ultra.py new file mode 100644 index 0000000..8440322 --- /dev/null +++ b/solax/inverters/x3_ultra.py @@ -0,0 +1,124 @@ +import voluptuous as vol + +from solax import utils +from solax.inverter import Inverter, InverterHttpClient, Method, ResponseParser +from solax.units import Total, Units +from solax.utils import div10, div100, pack_u16, to_signed, to_signed32 + + +class X3Ultra(Inverter): + # 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=9, max=10))), + }, + extra=vol.REMOVE_EXTRA, + ) + pwd: str = '' + url: str = '' + + @classmethod + def _build(cls, host, port, pwd="", params_in_query=True): + cls.pwd = pwd + cls.url = utils.to_url(host, port) + http_client = InverterHttpClient(cls.url, Method.POST, pwd).with_default_data() + + response_parser = ResponseParser(cls._schema, cls.response_decoder()) + return cls(http_client, response_parser) + + @classmethod + def build_all_variants(cls, host, port, pwd=""): + versions = [cls._build(host, port, pwd)] + return versions + + @classmethod + def _decode_run_mode(cls, run_mode): + return { + 0: "Waiting", + 1: "Checking", + 2: "Normal", + 3: "Fault", + 4: "Permanent Fault", + 5: "Updating", + 6: "EpsCheck", + 7: "Eps", + 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, div10), + "Grid 2 Current": (4, Units.A, div10), + "Grid 3 Current": (5, Units.A, 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), + "Pv1 Current": (12, Units.A, div10), + "Pv2 Current": (13, Units.A, div10), + "PV1 Power": (14, Units.W, to_signed), + "PV2 Power": (15, Units.W, to_signed), + "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, 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, div10), + "EPS 2 Current": (27, Units.A, div10), + "EPS 3 Current": (28, Units.A, 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), + "EPS Frequency": (32, Units.HZ, div100), + "Invert temperature": (46, Units.C, to_signed), + "Selfuse Power": (47, Units.W, to_signed), + "Radiator Temperature": (54, Units.C, to_signed), + "Yield Output Today": (70, Units.KWH, div10), + "Yield Input Total": (pack_u16(71, 72), Total(Units.KWH), div10), + "Yield Input Today": (73, Units.KWH, div10), + "Battery Discharge Total": (pack_u16(74, 75), Total(Units.KWH), div10), + "Battery Charge Total": (pack_u16(76, 77), Total(Units.KWH), div10), + "Output Energy Charge Today": (78, Units.KWH, div10), + "Input Energy Charge Today": (79, Units.KWH, div10), + "Pv Yield Total": (pack_u16(80, 81), Total(Units.KWH), div10), + "Solar Energy Today": (82, Units.KWH, div10), + "Eps Yield Total": (pack_u16(83, 84), Total(Units.KWH), div10), + "Eps Yield Today": (85, Units.KWH, div10), + "Feed in Energy Total": (pack_u16(86, 87), Units.KWH, div100), + "Consume Total": (pack_u16(88, 89), Total(Units.KWH), div100), + "Feed in Energy Today": (pack_u16(90, 91), Units.KWH, div100), + "Consume Today": (pack_u16(92, 93), Total(Units.KWH), div100), + "Battery 1 Capacity": (103, Units.PERCENT), + "Battery 1 Temperature": (105, Units.C, to_signed), + "Battery 1 Surplus Energy": (106, Units.KWH, div10), + "PV3 Voltage": (129, Units.V, div10), + "Pv3 Current": (130, Units.A, div10), + "PV3 Power": (131, Units.W, to_signed), + "Battery 2 Capacity": (140, Units.PERCENT), + "Battery 2 Temperature": (142, Units.C, to_signed), + "Battery 2 Surplus Energy": (143, Units.W, div10), + + } + + # pylint: enable=duplicate-code From fef787374b41be4c4e1633ad3cd147fbe2fa3bd0 Mon Sep 17 00:00:00 2001 From: panjialiang <371630856@qq.com> Date: Tue, 19 Dec 2023 20:02:42 +0800 Subject: [PATCH 2/3] add new inverter and AES encryption --- solax/discovery.py | 3 +-- solax/inverters/x1_mini_g3.py | 6 ------ 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/solax/discovery.py b/solax/discovery.py index ae35493..26b6a5a 100644 --- a/solax/discovery.py +++ b/solax/discovery.py @@ -83,8 +83,7 @@ def _task_handler(self, task): @classmethod async def _discovery_task(cls, i) -> Inverter: logging.info("Trying inverter %s", i) - final_data = await i.get_data() - + await i.get_data() return i async def discover(self, host, port, pwd="") -> Inverter: diff --git a/solax/inverters/x1_mini_g3.py b/solax/inverters/x1_mini_g3.py index bc62c5a..a3e3539 100644 --- a/solax/inverters/x1_mini_g3.py +++ b/solax/inverters/x1_mini_g3.py @@ -100,9 +100,3 @@ def response_decoder(cls): # pylint: enable=duplicate-code - -data = [2237, 78, 1745, 3627, 0, 48, 0, 1755, 0, 4999, 2, 120, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 4750, 5015, 2050, 2530, 4750, 5010, 1955, 30, 2760, 1, 0, 5150, 10, 0, 0, 0, 0, 0, 56, 0, 61, 0, 0, 32, 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] -print(data[34]) From 392ccbb7fc30a92bfc0150ca2bdf5893723fa4c3 Mon Sep 17 00:00:00 2001 From: panjialiang <371630856@qq.com> Date: Tue, 23 Jan 2024 14:35:15 +0800 Subject: [PATCH 3/3] try 3 times to make request and change inverter registry --- solax/discovery.py | 24 ++---------------------- solax/inverter.py | 8 ++++++-- 2 files changed, 8 insertions(+), 24 deletions(-) diff --git a/solax/discovery.py b/solax/discovery.py index 26b6a5a..e3f4d07 100644 --- a/solax/discovery.py +++ b/solax/discovery.py @@ -4,19 +4,9 @@ from solax.inverter import Inverter, InverterError from solax.inverters import ( - QVOLTHYBG33P, - X1, - X3, - X3V34, - X1Boost, X1HybridGen4, - X1Mini, - X1MiniV34, - X1Smart, X3HybridG4, X3MicProG2, - XHybrid, - J1EssHb, X1HybridG2, X1MiniG3, X1MiniG4, @@ -29,27 +19,17 @@ # registry of inverters REGISTRY = [ - XHybrid, - X3, - X3V34, X3HybridG4, - X1, - X1Mini, - X1MiniV34, - X1Smart, - QVOLTHYBG33P, - X1Boost, X1HybridGen4, X3MicProG2, - J1EssHb, X1HybridG2, + X1Ies, X1MiniG3, X1MiniG4, X1HybridLv, - X1Ies, X3Ies, X3Ultra, - X1BoostG4 + X1BoostG4, ] logging.basicConfig(level=logging.INFO) diff --git a/solax/inverter.py b/solax/inverter.py index 7a7cf78..3d00685 100644 --- a/solax/inverter.py +++ b/solax/inverter.py @@ -1,5 +1,4 @@ from typing import Dict, Tuple - import aiohttp import voluptuous as vol @@ -76,7 +75,12 @@ async def make_request(self) -> InverterResponse: Return instance of 'InverterResponse' Raise exception if unable to get data """ - raw_response = await self.http_client.request() + for i in range(3): + try: + raw_response = await self.http_client.request() + break + except aiohttp.ClientError as ex: + print('request error:', ex) str_raw_response = raw_response.decode('utf-8') if str_raw_response.startswith('{"code":'): pass