From 5c42aefd013571307d6fcfe0bb17167f56db5fa9 Mon Sep 17 00:00:00 2001 From: koyote Date: Fri, 4 Nov 2022 19:02:14 +1100 Subject: [PATCH 1/5] Add support for new Ginlong/Solis Wifi stick (S3-WIFI-ST) --- omnikinverter/models.py | 63 ++++++++++++++++++++++++++++++++++ omnikinverter/omnikinverter.py | 8 ++++- tests/fixtures/inverter.cgi | 1 + tests/fixtures/moniter.cgi | 1 + tests/test_models.py | 61 ++++++++++++++++++++++++++++++++ 5 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/inverter.cgi create mode 100644 tests/fixtures/moniter.cgi diff --git a/omnikinverter/models.py b/omnikinverter/models.py index e69a29a8..71d1cc57 100644 --- a/omnikinverter/models.py +++ b/omnikinverter/models.py @@ -200,6 +200,40 @@ def from_tcp(data: dict[str, Any]) -> Inverter: ), ) + @staticmethod + def from_cgi(data: str) -> Inverter: + """Return Inverter object from the Omnik Inverter response. + + Args: + data: The CGI (webscraping) data from the Omnik Inverter. + + Returns: + An Inverter object. + """ + + split_data = data.split(";") + if len(split_data) < 7: + raise OmnikInverterWrongSourceError( + "Your inverter has no data source from cgi." + ) + + def try_parse_float(item): + try: + return float(item) + except: + return None + + return Inverter( + serial_number=split_data[0], + model=split_data[2], + firmware=split_data[1], + firmware_slave=None, + solar_rated_power=None, + solar_current_power=try_parse_float(split_data[4]), + solar_energy_today=try_parse_float(split_data[5]), + solar_energy_total=try_parse_float(split_data[6]), + ) + @dataclass class Device: @@ -285,3 +319,32 @@ def get_value(search_key: str) -> Any: firmware=get_value("version"), ip_address=get_value("wanIp"), ) + + @staticmethod + def from_cgi(data: str) -> Device: + """Return Device object from the Omnik Inverter response. + + Args: + data: The CGI (webscraping) data from the Omnik Inverter. + + Returns: + A Device object. + """ + + split_data = data.split(";") + if len(split_data) < 9: + raise OmnikInverterWrongSourceError( + "Your inverter has no device data from cgi." + ) + + def try_parse_int(item): + try: + return int(item) + except: + return None + + return Device( + signal_quality=try_parse_int(split_data[8]), + firmware=split_data[1], + ip_address=split_data[9], + ) diff --git a/omnikinverter/omnikinverter.py b/omnikinverter/omnikinverter.py index 482f2509..6bfcbbe8 100644 --- a/omnikinverter/omnikinverter.py +++ b/omnikinverter/omnikinverter.py @@ -75,7 +75,7 @@ async def request( # Use big try to make sure manual session is always cleaned up try: - if self.source_type == "html" and ( + if (self.source_type == "html" or self.source_type == "cgi") and ( self.username is None or self.password is None ): raise OmnikInverterAuthError( @@ -179,6 +179,9 @@ async def inverter(self) -> Inverter: if self.source_type == "html": data = await self.request("status.html") return Inverter.from_html(data) + if self.source_type == "cgi": + data = await self.request("inverter.cgi", params={"t": "123"}) + return Inverter.from_cgi(data) if self.source_type == "javascript": data = await self.request("js/status.js") return Inverter.from_js(data) @@ -203,6 +206,9 @@ async def device(self) -> Device: if self.source_type == "html": data = await self.request("status.html") return Device.from_html(data) + if self.source_type == "cgi": + data = await self.request("moniter.cgi", params={"t": "123"}) + return Device.from_cgi(data) if self.source_type == "javascript": data = await self.request("js/status.js") return Device.from_js(data) diff --git a/tests/fixtures/inverter.cgi b/tests/fixtures/inverter.cgi new file mode 100644 index 00000000..5ed43d55 --- /dev/null +++ b/tests/fixtures/inverter.cgi @@ -0,0 +1 @@ +12345678910;3280031;205;38.4;780;23.799999;d;NO; \ No newline at end of file diff --git a/tests/fixtures/moniter.cgi b/tests/fixtures/moniter.cgi new file mode 100644 index 00000000..44fe0025 --- /dev/null +++ b/tests/fixtures/moniter.cgi @@ -0,0 +1 @@ +5A1227251CB01447;00010130;Disable;null;null;null;Enable;SSID NAME;60;192.168.0.106;BE:BA:DE:FF:1C:6B;Connected;Unconnected; \ No newline at end of file diff --git a/tests/test_models.py b/tests/test_models.py index cc84f9dd..1b164054 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -130,6 +130,67 @@ async def test_device_html(aresponses: ResponsesMockServer) -> None: assert device.firmware == "ME_08_0102_2.03" assert device.ip_address == "192.168.0.106" +@pytest.mark.asyncio +async def test_inverter_cgi(aresponses: ResponsesMockServer) -> None: + """Test request from an Inverter - CGI source.""" + aresponses.add( + "example.com", + "/inverter.cgi", + "GET", + aresponses.Response( + status=200, + headers={"Content-Type": "text/html"}, + text=load_fixtures("inverter.cgi"), + ), + ) + + async with aiohttp.ClientSession() as session: + client = OmnikInverter( + host="example.com", + source_type="cgi", + username="klaas", + password="supercool", + session=session, + ) + inverter: Inverter = await client.inverter() + assert inverter + assert inverter.serial_number == "12345678910" + assert inverter.firmware == "3280031" + assert inverter.firmware_slave is None + assert inverter.model == "205" + assert inverter.solar_rated_power is None + assert inverter.solar_current_power == 780 + assert inverter.solar_energy_today == 23.799999 + assert inverter.solar_energy_total is None + + +@pytest.mark.asyncio +async def test_device_cgi(aresponses: ResponsesMockServer) -> None: + """Test request from a Device - CGI source.""" + aresponses.add( + "example.com", + "/moniter.cgi", + "GET", + aresponses.Response( + status=200, + headers={"Content-Type": "text/html"}, + text=load_fixtures("moniter.cgi"), + ), + ) + + async with aiohttp.ClientSession() as session: + client = OmnikInverter( + host="example.com", + source_type="cgi", + username="klaas", + password="supercool", + session=session, + ) + device: Device = await client.device() + assert device + assert device.signal_quality == 60 + assert device.firmware == "00010130" + assert device.ip_address == "192.168.0.106" @pytest.mark.asyncio async def test_inverter_without_session(aresponses: ResponsesMockServer) -> None: From c9292d3fd28e036063f55af05b75eb6a2e6462d3 Mon Sep 17 00:00:00 2001 From: koyote Date: Fri, 4 Nov 2022 19:45:35 +1100 Subject: [PATCH 2/5] Update README to include CGI information --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 680d6698..5262a75c 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ A python package with which you can read the data from your Omnik Inverter. Keep | Omnik | Omniksol 3000TL | TCP | | Omnik | Omniksol 4000TL2 | JS | | Ginlong | Solis-DLS-WiFi | JSON/HTML | +| Ginlong | S3-WIFI-ST | CGI | | Hosola | 1500TL | JS | | Bosswerk | BW-MI300 | HTML | | Bosswerk | BW-MI600 | HTML | @@ -93,9 +94,12 @@ You can read the following data with this package: - Day Energy Production (kWh) - Total Energy Production (kWh) +On the `cgi` source type you can also find: +- Inverter temperature + On the `tcp` source type you can also find: -- Inverter temperature; +- Inverter temperature - Voltage and current for the DC input strings (up to 3) - Voltage, current, frequency and power for all AC outputs (also up to 3) - Total number of runtime hours. From 46ba13e0c7c5f593dd359e47ebd49fab766349e3 Mon Sep 17 00:00:00 2001 From: koyote Date: Fri, 4 Nov 2022 19:50:10 +1100 Subject: [PATCH 3/5] Add temperature support for cgi --- omnikinverter/models.py | 5 +++-- tests/test_models.py | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/omnikinverter/models.py b/omnikinverter/models.py index 71d1cc57..e42b0aad 100644 --- a/omnikinverter/models.py +++ b/omnikinverter/models.py @@ -215,9 +215,9 @@ def from_cgi(data: str) -> Inverter: if len(split_data) < 7: raise OmnikInverterWrongSourceError( "Your inverter has no data source from cgi." - ) + ) - def try_parse_float(item): + def try_parse_float(item) -> float | None: try: return float(item) except: @@ -232,6 +232,7 @@ def try_parse_float(item): solar_current_power=try_parse_float(split_data[4]), solar_energy_today=try_parse_float(split_data[5]), solar_energy_total=try_parse_float(split_data[6]), + temperature=try_parse_float(split_data[3]) ) diff --git a/tests/test_models.py b/tests/test_models.py index 1b164054..26fd693c 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -162,6 +162,7 @@ async def test_inverter_cgi(aresponses: ResponsesMockServer) -> None: assert inverter.solar_current_power == 780 assert inverter.solar_energy_today == 23.799999 assert inverter.solar_energy_total is None + assert inverter.temperature == 38.4 @pytest.mark.asyncio From 1f8a75a1caf84acea0ce11f9069ab3d0f117bc42 Mon Sep 17 00:00:00 2001 From: koyote Date: Fri, 4 Nov 2022 19:57:30 +1100 Subject: [PATCH 4/5] code cleanup --- omnikinverter/models.py | 24 +++++++++++++++--------- tests/fixtures/inverter.cgi | 2 +- tests/fixtures/moniter.cgi | 2 +- tests/test_models.py | 2 ++ 4 files changed, 19 insertions(+), 11 deletions(-) diff --git a/omnikinverter/models.py b/omnikinverter/models.py index e42b0aad..b9b72d4f 100644 --- a/omnikinverter/models.py +++ b/omnikinverter/models.py @@ -214,25 +214,31 @@ def from_cgi(data: str) -> Inverter: split_data = data.split(";") if len(split_data) < 7: raise OmnikInverterWrongSourceError( - "Your inverter has no data source from cgi." - ) + "Your inverter has no data source from cgi." + ) - def try_parse_float(item) -> float | None: + def try_parse_float(item: str) -> float | None: try: return float(item) except: return None + def try_parse_int(item: str) -> int | None: + try: + return int(item) + except: + return None + return Inverter( serial_number=split_data[0], model=split_data[2], firmware=split_data[1], firmware_slave=None, solar_rated_power=None, - solar_current_power=try_parse_float(split_data[4]), + solar_current_power=try_parse_int(split_data[4]), solar_energy_today=try_parse_float(split_data[5]), solar_energy_total=try_parse_float(split_data[6]), - temperature=try_parse_float(split_data[3]) + temperature=try_parse_float(split_data[3]), ) @@ -335,15 +341,15 @@ def from_cgi(data: str) -> Device: split_data = data.split(";") if len(split_data) < 9: raise OmnikInverterWrongSourceError( - "Your inverter has no device data from cgi." - ) + "Your inverter has no device data from cgi." + ) - def try_parse_int(item): + def try_parse_int(item: str) -> int | None: try: return int(item) except: return None - + return Device( signal_quality=try_parse_int(split_data[8]), firmware=split_data[1], diff --git a/tests/fixtures/inverter.cgi b/tests/fixtures/inverter.cgi index 5ed43d55..1ce9522f 100644 --- a/tests/fixtures/inverter.cgi +++ b/tests/fixtures/inverter.cgi @@ -1 +1 @@ -12345678910;3280031;205;38.4;780;23.799999;d;NO; \ No newline at end of file +12345678910;3280031;205;38.4;780;23.799999;d;NO; diff --git a/tests/fixtures/moniter.cgi b/tests/fixtures/moniter.cgi index 44fe0025..721a2a90 100644 --- a/tests/fixtures/moniter.cgi +++ b/tests/fixtures/moniter.cgi @@ -1 +1 @@ -5A1227251CB01447;00010130;Disable;null;null;null;Enable;SSID NAME;60;192.168.0.106;BE:BA:DE:FF:1C:6B;Connected;Unconnected; \ No newline at end of file +5A1227251CB01447;00010130;Disable;null;null;null;Enable;SSID NAME;60;192.168.0.106;BE:BA:DE:FF:1C:6B;Connected;Unconnected; diff --git a/tests/test_models.py b/tests/test_models.py index 26fd693c..ced823ca 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -130,6 +130,7 @@ async def test_device_html(aresponses: ResponsesMockServer) -> None: assert device.firmware == "ME_08_0102_2.03" assert device.ip_address == "192.168.0.106" + @pytest.mark.asyncio async def test_inverter_cgi(aresponses: ResponsesMockServer) -> None: """Test request from an Inverter - CGI source.""" @@ -193,6 +194,7 @@ async def test_device_cgi(aresponses: ResponsesMockServer) -> None: assert device.firmware == "00010130" assert device.ip_address == "192.168.0.106" + @pytest.mark.asyncio async def test_inverter_without_session(aresponses: ResponsesMockServer) -> None: """Test request from an Inverter - HTML source and without session.""" From 6bce5f1fb41131e2cfe5a6b8d69640cc2c272e4f Mon Sep 17 00:00:00 2001 From: koyote Date: Fri, 11 Nov 2022 14:02:47 +1100 Subject: [PATCH 5/5] Remove dot from README list item --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5262a75c..1093b3e9 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ On the `tcp` source type you can also find: - Inverter temperature - Voltage and current for the DC input strings (up to 3) - Voltage, current, frequency and power for all AC outputs (also up to 3) -- Total number of runtime hours. +- Total number of runtime hours ### Device