goodwe 0.3.5__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {goodwe-0.3.5/goodwe.egg-info → goodwe-0.4.0}/PKG-INFO +12 -7
- {goodwe-0.3.5 → goodwe-0.4.0}/README.md +11 -6
- goodwe-0.4.0/VERSION +1 -0
- {goodwe-0.3.5 → goodwe-0.4.0}/goodwe/__init__.py +32 -37
- {goodwe-0.3.5 → goodwe-0.4.0}/goodwe/const.py +1 -0
- {goodwe-0.3.5 → goodwe-0.4.0}/goodwe/dt.py +8 -8
- {goodwe-0.3.5 → goodwe-0.4.0}/goodwe/es.py +8 -9
- {goodwe-0.3.5 → goodwe-0.4.0}/goodwe/et.py +30 -32
- {goodwe-0.3.5 → goodwe-0.4.0}/goodwe/inverter.py +35 -38
- {goodwe-0.3.5 → goodwe-0.4.0}/goodwe/modbus.py +106 -6
- goodwe-0.4.0/goodwe/protocol.py +618 -0
- {goodwe-0.3.5 → goodwe-0.4.0}/goodwe/sensor.py +5 -2
- {goodwe-0.3.5 → goodwe-0.4.0/goodwe.egg-info}/PKG-INFO +12 -7
- {goodwe-0.3.5 → goodwe-0.4.0}/tests/test_dt.py +1 -1
- {goodwe-0.3.5 → goodwe-0.4.0}/tests/test_es.py +1 -1
- {goodwe-0.3.5 → goodwe-0.4.0}/tests/test_et.py +8 -33
- goodwe-0.4.0/tests/test_modbus.py +111 -0
- {goodwe-0.3.5 → goodwe-0.4.0}/tests/test_protocol.py +42 -33
- goodwe-0.3.5/VERSION +0 -1
- goodwe-0.3.5/goodwe/protocol.py +0 -338
- goodwe-0.3.5/tests/test_modbus.py +0 -66
- {goodwe-0.3.5 → goodwe-0.4.0}/LICENSE +0 -0
- {goodwe-0.3.5 → goodwe-0.4.0}/goodwe/exceptions.py +0 -0
- {goodwe-0.3.5 → goodwe-0.4.0}/goodwe/model.py +0 -0
- {goodwe-0.3.5 → goodwe-0.4.0}/goodwe.egg-info/SOURCES.txt +0 -0
- {goodwe-0.3.5 → goodwe-0.4.0}/goodwe.egg-info/dependency_links.txt +0 -0
- {goodwe-0.3.5 → goodwe-0.4.0}/goodwe.egg-info/top_level.txt +0 -0
- {goodwe-0.3.5 → goodwe-0.4.0}/pyproject.toml +0 -0
- {goodwe-0.3.5 → goodwe-0.4.0}/setup.cfg +0 -0
- {goodwe-0.3.5 → goodwe-0.4.0}/tests/test_sensor.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: goodwe
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Read data from GoodWe inverter via local network
|
|
5
5
|
Home-page: https://github.com/marcelblijleven/goodwe
|
|
6
6
|
Author: Martin Letenay, Marcel Blijleven
|
|
@@ -32,15 +32,21 @@ License-File: LICENSE
|
|
|
32
32
|
Library for connecting to GoodWe inverter over local network and retrieving runtime sensor values and configuration
|
|
33
33
|
parameters.
|
|
34
34
|
|
|
35
|
-
It has been reported to work
|
|
36
|
-
work
|
|
37
|
-
protocols.
|
|
35
|
+
It has been reported to work with GoodWe ET, EH, BT, BH, ES, EM, BP, DT, MS, D-NS, and XS families of inverters. It
|
|
36
|
+
should work with other inverters as well, as long as they listen on UDP port 8899 and respond to one of supported
|
|
37
|
+
communication protocols.
|
|
38
|
+
In general, if you can connect to your inverter with the official mobile app (SolarGo/PvMaster) over Wi-Fi (not
|
|
39
|
+
bluetooth), this library should work.
|
|
38
40
|
|
|
39
41
|
(If you can't communicate with the inverter despite your model is listed above, it is possible you have old ARM firmware
|
|
40
42
|
version. You should ask manufacturer support to upgrade your ARM firmware (not just inverter firmware) to be able to
|
|
41
|
-
communicate with the
|
|
43
|
+
communicate with the inverter via UDP.)
|
|
42
44
|
|
|
43
|
-
White-label (GoodWe manufactured) inverters may work as well, e.g. General Electric GEP (PSB, PSC) and GEH models
|
|
45
|
+
White-label (GoodWe manufactured) inverters may work as well, e.g. General Electric GEP (PSB, PSC) and GEH models are
|
|
46
|
+
know to work properly.
|
|
47
|
+
|
|
48
|
+
Since v0.4.x the library also supports standard Modbus/TCP over port 502.
|
|
49
|
+
This protocol is supported by the V2.0 version of LAN+WiFi communication dongle (model WLA0000-01-00P).
|
|
44
50
|
|
|
45
51
|
## Usage
|
|
46
52
|
|
|
@@ -72,4 +78,3 @@ asyncio.run(get_runtime_data())
|
|
|
72
78
|
- https://github.com/mletenay/home-assistant-goodwe-inverter
|
|
73
79
|
- https://github.com/yasko-pv/modbus-log
|
|
74
80
|
- https://github.com/tkubec/GoodWe
|
|
75
|
-
- https://github.com/OpenEMS/openems
|
|
@@ -8,15 +8,21 @@
|
|
|
8
8
|
Library for connecting to GoodWe inverter over local network and retrieving runtime sensor values and configuration
|
|
9
9
|
parameters.
|
|
10
10
|
|
|
11
|
-
It has been reported to work
|
|
12
|
-
work
|
|
13
|
-
protocols.
|
|
11
|
+
It has been reported to work with GoodWe ET, EH, BT, BH, ES, EM, BP, DT, MS, D-NS, and XS families of inverters. It
|
|
12
|
+
should work with other inverters as well, as long as they listen on UDP port 8899 and respond to one of supported
|
|
13
|
+
communication protocols.
|
|
14
|
+
In general, if you can connect to your inverter with the official mobile app (SolarGo/PvMaster) over Wi-Fi (not
|
|
15
|
+
bluetooth), this library should work.
|
|
14
16
|
|
|
15
17
|
(If you can't communicate with the inverter despite your model is listed above, it is possible you have old ARM firmware
|
|
16
18
|
version. You should ask manufacturer support to upgrade your ARM firmware (not just inverter firmware) to be able to
|
|
17
|
-
communicate with the
|
|
19
|
+
communicate with the inverter via UDP.)
|
|
18
20
|
|
|
19
|
-
White-label (GoodWe manufactured) inverters may work as well, e.g. General Electric GEP (PSB, PSC) and GEH models
|
|
21
|
+
White-label (GoodWe manufactured) inverters may work as well, e.g. General Electric GEP (PSB, PSC) and GEH models are
|
|
22
|
+
know to work properly.
|
|
23
|
+
|
|
24
|
+
Since v0.4.x the library also supports standard Modbus/TCP over port 502.
|
|
25
|
+
This protocol is supported by the V2.0 version of LAN+WiFi communication dongle (model WLA0000-01-00P).
|
|
20
26
|
|
|
21
27
|
## Usage
|
|
22
28
|
|
|
@@ -48,4 +54,3 @@ asyncio.run(get_runtime_data())
|
|
|
48
54
|
- https://github.com/mletenay/home-assistant-goodwe-inverter
|
|
49
55
|
- https://github.com/yasko-pv/modbus-log
|
|
50
56
|
- https://github.com/tkubec/GoodWe
|
|
51
|
-
- https://github.com/OpenEMS/openems
|
goodwe-0.4.0/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.4.0
|
|
@@ -2,8 +2,8 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import logging
|
|
5
|
-
from typing import Type
|
|
6
5
|
|
|
6
|
+
from .const import GOODWE_TCP_PORT, GOODWE_UDP_PORT
|
|
7
7
|
from .dt import DT
|
|
8
8
|
from .es import ES
|
|
9
9
|
from .et import ET
|
|
@@ -26,8 +26,8 @@ DISCOVERY_COMMAND = Aa55ProtocolCommand("010200", "0182")
|
|
|
26
26
|
_SUPPORTED_PROTOCOLS = [ET, DT, ES]
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
async def connect(host: str, family: str = None, comm_addr: int = 0, timeout: int = 1,
|
|
30
|
-
do_discover: bool = True) -> Inverter:
|
|
29
|
+
async def connect(host: str, port: int = GOODWE_UDP_PORT, family: str = None, comm_addr: int = 0, timeout: int = 1,
|
|
30
|
+
retries: int = 3, do_discover: bool = True) -> Inverter:
|
|
31
31
|
"""Contact the inverter at the specified host/port and answer appropriate Inverter instance.
|
|
32
32
|
|
|
33
33
|
The specific inverter family/type will be detected automatically, but it can be passed explicitly.
|
|
@@ -41,24 +41,24 @@ async def connect(host: str, family: str = None, comm_addr: int = 0, timeout: in
|
|
|
41
41
|
|
|
42
42
|
Raise InverterError if unable to contact or recognise supported inverter.
|
|
43
43
|
"""
|
|
44
|
-
if family in ET_FAMILY:
|
|
45
|
-
inv = ET(host, comm_addr, timeout, retries)
|
|
44
|
+
if family in ET_FAMILY or port == GOODWE_TCP_PORT:
|
|
45
|
+
inv = ET(host, port, comm_addr, timeout, retries)
|
|
46
46
|
elif family in ES_FAMILY:
|
|
47
|
-
inv = ES(host, comm_addr, timeout, retries)
|
|
47
|
+
inv = ES(host, port, comm_addr, timeout, retries)
|
|
48
48
|
elif family in DT_FAMILY:
|
|
49
|
-
inv = DT(host, comm_addr, timeout, retries)
|
|
49
|
+
inv = DT(host, port, comm_addr, timeout, retries)
|
|
50
50
|
elif do_discover:
|
|
51
|
-
return await discover(host, timeout, retries)
|
|
51
|
+
return await discover(host, port, timeout, retries)
|
|
52
52
|
else:
|
|
53
53
|
raise InverterError("Specify either an inverter family or set do_discover True")
|
|
54
54
|
|
|
55
|
-
logger.debug("Connecting to %s family inverter at %s.", family, host)
|
|
55
|
+
logger.debug("Connecting to %s family inverter at %s:%s.", family, host, port)
|
|
56
56
|
await inv.read_device_info()
|
|
57
57
|
logger.debug("Connected to inverter %s, S/N:%s.", inv.model_name, inv.serial_number)
|
|
58
58
|
return inv
|
|
59
59
|
|
|
60
60
|
|
|
61
|
-
async def discover(host: str, timeout: int = 1, retries: int = 3) -> Inverter:
|
|
61
|
+
async def discover(host: str, port: int = GOODWE_UDP_PORT, timeout: int = 1, retries: int = 3) -> Inverter:
|
|
62
62
|
"""Contact the inverter at the specified value and answer appropriate Inverter instance
|
|
63
63
|
|
|
64
64
|
Raise InverterError if unable to contact or recognise supported inverter
|
|
@@ -67,28 +67,33 @@ async def discover(host: str, timeout: int = 1, retries: int = 3) -> Inverter:
|
|
|
67
67
|
|
|
68
68
|
# Try the common AA55C07F0102000241 command first and detect inverter type from serial_number
|
|
69
69
|
try:
|
|
70
|
-
logger.debug("Probing inverter at %s.", host)
|
|
71
|
-
response = await DISCOVERY_COMMAND.execute(host, timeout, retries)
|
|
70
|
+
logger.debug("Probing inverter at %s:%s.", host, port)
|
|
71
|
+
response = await DISCOVERY_COMMAND.execute(UdpInverterProtocol(host, port, timeout, retries))
|
|
72
72
|
response = response.response_data()
|
|
73
73
|
model_name = response[5:15].decode("ascii").rstrip()
|
|
74
74
|
serial_number = response[31:47].decode("ascii")
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
i: Inverter | None = None
|
|
77
77
|
for model_tag in ET_MODEL_TAGS:
|
|
78
78
|
if model_tag in serial_number:
|
|
79
79
|
logger.debug("Detected ET/EH/BT/BH/GEH inverter %s, S/N:%s.", model_name, serial_number)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
80
|
+
i = ET(host, port, 0, timeout, retries)
|
|
81
|
+
break
|
|
82
|
+
if not i:
|
|
83
|
+
for model_tag in ES_MODEL_TAGS:
|
|
84
|
+
if model_tag in serial_number:
|
|
85
|
+
logger.debug("Detected ES/EM/BP inverter %s, S/N:%s.", model_name, serial_number)
|
|
86
|
+
i = ES(host, port, 0, timeout, retries)
|
|
87
|
+
break
|
|
88
|
+
if not i:
|
|
89
|
+
for model_tag in DT_MODEL_TAGS:
|
|
90
|
+
if model_tag in serial_number:
|
|
91
|
+
logger.debug("Detected DT/MS/D-NS/XS/GEP inverter %s, S/N:%s.", model_name, serial_number)
|
|
92
|
+
i = DT(host, port, 0, timeout, retries)
|
|
93
|
+
break
|
|
94
|
+
if i:
|
|
91
95
|
await i.read_device_info()
|
|
96
|
+
logger.debug("Connected to inverter %s, S/N:%s.", i.model_name, i.serial_number)
|
|
92
97
|
return i
|
|
93
98
|
|
|
94
99
|
except InverterError as ex:
|
|
@@ -96,7 +101,7 @@ async def discover(host: str, timeout: int = 1, retries: int = 3) -> Inverter:
|
|
|
96
101
|
|
|
97
102
|
# Probe inverter specific protocols
|
|
98
103
|
for inv in _SUPPORTED_PROTOCOLS:
|
|
99
|
-
i = inv(host, 0, timeout, retries)
|
|
104
|
+
i = inv(host, port, 0, timeout, retries)
|
|
100
105
|
try:
|
|
101
106
|
logger.debug("Probing %s inverter at %s.", inv.__name__, host)
|
|
102
107
|
await i.read_device_info()
|
|
@@ -119,22 +124,12 @@ async def search_inverters() -> bytes:
|
|
|
119
124
|
Raise InverterError if unable to contact any inverter
|
|
120
125
|
"""
|
|
121
126
|
logger.debug("Searching inverters by broadcast to port 48899")
|
|
122
|
-
loop = asyncio.get_running_loop()
|
|
123
127
|
command = ProtocolCommand("WIFIKIT-214028-READ".encode("utf-8"), lambda r: True)
|
|
124
|
-
response_future = loop.create_future()
|
|
125
|
-
transport, _ = await loop.create_datagram_endpoint(
|
|
126
|
-
lambda: UdpInverterProtocol(response_future, command, 1, 3),
|
|
127
|
-
remote_addr=("255.255.255.255", 48899),
|
|
128
|
-
allow_broadcast=True,
|
|
129
|
-
)
|
|
130
128
|
try:
|
|
131
|
-
await
|
|
132
|
-
result = response_future.result()
|
|
129
|
+
result = await command.execute(UdpInverterProtocol("255.255.255.255", 48899, 1, 0))
|
|
133
130
|
if result is not None:
|
|
134
|
-
return result
|
|
131
|
+
return result.response_data()
|
|
135
132
|
else:
|
|
136
133
|
raise InverterError("No response received to broadcast request.")
|
|
137
134
|
except asyncio.CancelledError:
|
|
138
135
|
raise InverterError("No valid response received to broadcast request.") from None
|
|
139
|
-
finally:
|
|
140
|
-
transport.close()
|
|
@@ -7,7 +7,7 @@ from .inverter import Inverter
|
|
|
7
7
|
from .inverter import OperationMode
|
|
8
8
|
from .inverter import SensorKind as Kind
|
|
9
9
|
from .model import is_3_mppt, is_single_phase
|
|
10
|
-
from .protocol import ProtocolCommand
|
|
10
|
+
from .protocol import ProtocolCommand
|
|
11
11
|
from .sensor import *
|
|
12
12
|
|
|
13
13
|
|
|
@@ -122,13 +122,13 @@ class DT(Inverter):
|
|
|
122
122
|
Integer("grid_export_limit", 40336, "Grid Export Limit", "%", Kind.GRID),
|
|
123
123
|
)
|
|
124
124
|
|
|
125
|
-
def __init__(self, host: str, comm_addr: int = 0, timeout: int = 1, retries: int = 3):
|
|
126
|
-
super().__init__(host, comm_addr, timeout, retries)
|
|
125
|
+
def __init__(self, host: str, port: int, comm_addr: int = 0, timeout: int = 1, retries: int = 3):
|
|
126
|
+
super().__init__(host, port, comm_addr, timeout, retries)
|
|
127
127
|
if not self.comm_addr:
|
|
128
128
|
# Set the default inverter address
|
|
129
129
|
self.comm_addr = 0x7f
|
|
130
|
-
self._READ_DEVICE_VERSION_INFO: ProtocolCommand =
|
|
131
|
-
self._READ_DEVICE_RUNNING_DATA: ProtocolCommand =
|
|
130
|
+
self._READ_DEVICE_VERSION_INFO: ProtocolCommand = self._read_command(0x7531, 0x0028)
|
|
131
|
+
self._READ_DEVICE_RUNNING_DATA: ProtocolCommand = self._read_command(0x7594, 0x0049)
|
|
132
132
|
self._sensors = self.__all_sensors
|
|
133
133
|
self._settings: dict[str, Sensor] = {s.id_: s for s in self.__all_settings}
|
|
134
134
|
|
|
@@ -180,7 +180,7 @@ class DT(Inverter):
|
|
|
180
180
|
if not setting:
|
|
181
181
|
raise ValueError(f'Unknown setting "{setting_id}"')
|
|
182
182
|
count = (setting.size_ + (setting.size_ % 2)) // 2
|
|
183
|
-
response = await self._read_from_socket(
|
|
183
|
+
response = await self._read_from_socket(self._read_command(setting.offset, count))
|
|
184
184
|
return setting.read_value(response)
|
|
185
185
|
|
|
186
186
|
async def write_setting(self, setting_id: str, value: Any):
|
|
@@ -190,9 +190,9 @@ class DT(Inverter):
|
|
|
190
190
|
raw_value = setting.encode_value(value)
|
|
191
191
|
if len(raw_value) <= 2:
|
|
192
192
|
value = int.from_bytes(raw_value, byteorder="big", signed=True)
|
|
193
|
-
await self._read_from_socket(
|
|
193
|
+
await self._read_from_socket(self._write_command(setting.offset, value))
|
|
194
194
|
else:
|
|
195
|
-
await self._read_from_socket(
|
|
195
|
+
await self._read_from_socket(self._write_multi_command(setting.offset, raw_value))
|
|
196
196
|
|
|
197
197
|
async def read_settings_data(self) -> Dict[str, Any]:
|
|
198
198
|
data = {}
|
|
@@ -7,8 +7,7 @@ from .exceptions import InverterError
|
|
|
7
7
|
from .inverter import Inverter
|
|
8
8
|
from .inverter import OperationMode
|
|
9
9
|
from .inverter import SensorKind as Kind
|
|
10
|
-
from .protocol import ProtocolCommand, Aa55ProtocolCommand, Aa55ReadCommand, Aa55WriteCommand, Aa55WriteMultiCommand
|
|
11
|
-
ModbusReadCommand, ModbusWriteCommand, ModbusWriteMultiCommand
|
|
10
|
+
from .protocol import ProtocolCommand, Aa55ProtocolCommand, Aa55ReadCommand, Aa55WriteCommand, Aa55WriteMultiCommand
|
|
12
11
|
from .sensor import *
|
|
13
12
|
|
|
14
13
|
logger = logging.getLogger(__name__)
|
|
@@ -168,8 +167,8 @@ class ES(Inverter):
|
|
|
168
167
|
ByteH("eco_mode_4_switch", 47567, "Eco Mode Group 4 Switch"),
|
|
169
168
|
)
|
|
170
169
|
|
|
171
|
-
def __init__(self, host: str, comm_addr: int = 0, timeout: int = 1, retries: int = 3):
|
|
172
|
-
super().__init__(host, comm_addr, timeout, retries)
|
|
170
|
+
def __init__(self, host: str, port: int, comm_addr: int = 0, timeout: int = 1, retries: int = 3):
|
|
171
|
+
super().__init__(host, port, comm_addr, timeout, retries)
|
|
173
172
|
if not self.comm_addr:
|
|
174
173
|
# Set the default inverter address
|
|
175
174
|
self.comm_addr = 0xf7
|
|
@@ -228,7 +227,7 @@ class ES(Inverter):
|
|
|
228
227
|
async def _read_setting(self, setting: Sensor) -> Any:
|
|
229
228
|
count = (setting.size_ + (setting.size_ % 2)) // 2
|
|
230
229
|
if self._is_modbus_setting(setting):
|
|
231
|
-
response = await self._read_from_socket(
|
|
230
|
+
response = await self._read_from_socket(self._read_command(setting.offset, count))
|
|
232
231
|
return setting.read_value(response)
|
|
233
232
|
else:
|
|
234
233
|
response = await self._read_from_socket(Aa55ReadCommand(setting.offset, count))
|
|
@@ -249,7 +248,7 @@ class ES(Inverter):
|
|
|
249
248
|
if setting.size_ == 1:
|
|
250
249
|
# modbus can address/store only 16 bit values, read the other 8 bytes
|
|
251
250
|
if self._is_modbus_setting(setting):
|
|
252
|
-
response = await self._read_from_socket(
|
|
251
|
+
response = await self._read_from_socket(self._read_command(setting.offset, 1))
|
|
253
252
|
raw_value = setting.encode_value(value, response.response_data()[0:2])
|
|
254
253
|
else:
|
|
255
254
|
response = await self._read_from_socket(Aa55ReadCommand(setting.offset, 1))
|
|
@@ -259,12 +258,12 @@ class ES(Inverter):
|
|
|
259
258
|
if len(raw_value) <= 2:
|
|
260
259
|
value = int.from_bytes(raw_value, byteorder="big", signed=True)
|
|
261
260
|
if self._is_modbus_setting(setting):
|
|
262
|
-
await self._read_from_socket(
|
|
261
|
+
await self._read_from_socket(self._write_command(setting.offset, value))
|
|
263
262
|
else:
|
|
264
263
|
await self._read_from_socket(Aa55WriteCommand(setting.offset, value))
|
|
265
264
|
else:
|
|
266
265
|
if self._is_modbus_setting(setting):
|
|
267
|
-
await self._read_from_socket(
|
|
266
|
+
await self._read_from_socket(self._write_multi_command(setting.offset, raw_value))
|
|
268
267
|
else:
|
|
269
268
|
await self._read_from_socket(Aa55WriteMultiCommand(setting.offset, raw_value))
|
|
270
269
|
|
|
@@ -291,7 +290,7 @@ class ES(Inverter):
|
|
|
291
290
|
result.remove(OperationMode.ECO_DISCHARGE)
|
|
292
291
|
return tuple(result)
|
|
293
292
|
|
|
294
|
-
async def get_operation_mode(self) -> OperationMode:
|
|
293
|
+
async def get_operation_mode(self) -> OperationMode | None:
|
|
295
294
|
mode_id = await self.read_setting('work_mode')
|
|
296
295
|
try:
|
|
297
296
|
mode = OperationMode(mode_id)
|
|
@@ -3,12 +3,12 @@ from __future__ import annotations
|
|
|
3
3
|
import logging
|
|
4
4
|
from typing import Tuple
|
|
5
5
|
|
|
6
|
-
from .exceptions import
|
|
6
|
+
from .exceptions import RequestRejectedException
|
|
7
7
|
from .inverter import Inverter
|
|
8
8
|
from .inverter import OperationMode
|
|
9
9
|
from .inverter import SensorKind as Kind
|
|
10
10
|
from .model import is_2_battery, is_4_mppt, is_745_platform, is_single_phase
|
|
11
|
-
from .protocol import ProtocolCommand
|
|
11
|
+
from .protocol import ProtocolCommand
|
|
12
12
|
from .sensor import *
|
|
13
13
|
|
|
14
14
|
logger = logging.getLogger(__name__)
|
|
@@ -152,6 +152,10 @@ class ET(Inverter):
|
|
|
152
152
|
read_bytes4_signed(data, 35182) -
|
|
153
153
|
read_bytes2_signed(data, 35140),
|
|
154
154
|
"House Consumption", "W", Kind.AC),
|
|
155
|
+
|
|
156
|
+
# Power4S("pbattery2", 35264, "Battery2 Power", Kind.BAT),
|
|
157
|
+
# Integer("battery2_mode", 35266, "Battery2 Mode code", "", Kind.BAT),
|
|
158
|
+
# Enum2("battery2_mode_label", 35184, BATTERY_MODES, "Battery2 Mode", Kind.BAT),
|
|
155
159
|
)
|
|
156
160
|
|
|
157
161
|
# Modbus registers from offset 0x9088 (37000)
|
|
@@ -405,18 +409,18 @@ class ET(Inverter):
|
|
|
405
409
|
Integer("eco_mode_enable", 47612, "Eco Mode Switch"),
|
|
406
410
|
)
|
|
407
411
|
|
|
408
|
-
def __init__(self, host: str, comm_addr: int = 0, timeout: int = 1, retries: int = 3):
|
|
409
|
-
super().__init__(host, comm_addr, timeout, retries)
|
|
412
|
+
def __init__(self, host: str, port: int, comm_addr: int = 0, timeout: int = 1, retries: int = 3):
|
|
413
|
+
super().__init__(host, port, comm_addr, timeout, retries)
|
|
410
414
|
if not self.comm_addr:
|
|
411
415
|
# Set the default inverter address
|
|
412
416
|
self.comm_addr = 0xf7
|
|
413
|
-
self._READ_DEVICE_VERSION_INFO: ProtocolCommand =
|
|
414
|
-
self._READ_RUNNING_DATA: ProtocolCommand =
|
|
415
|
-
self._READ_METER_DATA: ProtocolCommand =
|
|
416
|
-
self._READ_METER_DATA_EXTENDED: ProtocolCommand =
|
|
417
|
-
self._READ_BATTERY_INFO: ProtocolCommand =
|
|
418
|
-
self._READ_BATTERY2_INFO: ProtocolCommand =
|
|
419
|
-
self._READ_MPPT_DATA: ProtocolCommand =
|
|
417
|
+
self._READ_DEVICE_VERSION_INFO: ProtocolCommand = self._read_command(0x88b8, 0x0021)
|
|
418
|
+
self._READ_RUNNING_DATA: ProtocolCommand = self._read_command(0x891c, 0x007d)
|
|
419
|
+
self._READ_METER_DATA: ProtocolCommand = self._read_command(0x8ca0, 0x2d)
|
|
420
|
+
self._READ_METER_DATA_EXTENDED: ProtocolCommand = self._read_command(0x8ca0, 0x3a)
|
|
421
|
+
self._READ_BATTERY_INFO: ProtocolCommand = self._read_command(0x9088, 0x0018)
|
|
422
|
+
self._READ_BATTERY2_INFO: ProtocolCommand = self._read_command(0x9858, 0x0016)
|
|
423
|
+
self._READ_MPPT_DATA: ProtocolCommand = self._read_command(0x89e5, 0x3d)
|
|
420
424
|
self._has_eco_mode_v2: bool = True
|
|
421
425
|
self._has_peak_shaving: bool = True
|
|
422
426
|
self._has_battery: bool = True
|
|
@@ -478,27 +482,21 @@ class ET(Inverter):
|
|
|
478
482
|
|
|
479
483
|
# Check and add EcoModeV2 settings added in (ETU fw 19)
|
|
480
484
|
try:
|
|
481
|
-
await self._read_from_socket(
|
|
485
|
+
await self._read_from_socket(self._read_command(47547, 6))
|
|
482
486
|
self._settings.update({s.id_: s for s in self.__settings_arm_fw_19})
|
|
483
487
|
except RequestRejectedException as ex:
|
|
484
488
|
if ex.message == 'ILLEGAL DATA ADDRESS':
|
|
485
|
-
logger.debug("EcoModeV2 settings
|
|
489
|
+
logger.debug("Cannot read EcoModeV2 settings, using to EcoModeV1.")
|
|
486
490
|
self._has_eco_mode_v2 = False
|
|
487
|
-
except RequestFailedException as ex:
|
|
488
|
-
logger.debug("Cannot read EcoModeV2 settings, switching to EcoModeV1.")
|
|
489
|
-
self._has_eco_mode_v2 = False
|
|
490
491
|
|
|
491
492
|
# Check and add Peak Shaving settings added in (ETU fw 22)
|
|
492
493
|
try:
|
|
493
|
-
await self._read_from_socket(
|
|
494
|
+
await self._read_from_socket(self._read_command(47589, 6))
|
|
494
495
|
self._settings.update({s.id_: s for s in self.__settings_arm_fw_22})
|
|
495
496
|
except RequestRejectedException as ex:
|
|
496
497
|
if ex.message == 'ILLEGAL DATA ADDRESS':
|
|
497
|
-
logger.debug("PeakShaving setting
|
|
498
|
+
logger.debug("Cannot read PeakShaving setting, disabling it.")
|
|
498
499
|
self._has_peak_shaving = False
|
|
499
|
-
except RequestFailedException as ex:
|
|
500
|
-
logger.debug("Cannot read _has_peak_shaving settings, disabling it.")
|
|
501
|
-
self._has_peak_shaving = False
|
|
502
500
|
|
|
503
501
|
async def read_runtime_data(self) -> Dict[str, Any]:
|
|
504
502
|
response = await self._read_from_socket(self._READ_RUNNING_DATA)
|
|
@@ -511,7 +509,7 @@ class ET(Inverter):
|
|
|
511
509
|
data.update(self._map_response(response, self._sensors_battery))
|
|
512
510
|
except RequestRejectedException as ex:
|
|
513
511
|
if ex.message == 'ILLEGAL DATA ADDRESS':
|
|
514
|
-
logger.warning("
|
|
512
|
+
logger.warning("Cannot read battery values, disabling further attempts.")
|
|
515
513
|
self._has_battery = False
|
|
516
514
|
else:
|
|
517
515
|
raise ex
|
|
@@ -522,7 +520,7 @@ class ET(Inverter):
|
|
|
522
520
|
self._map_response(response, self._sensors_battery2))
|
|
523
521
|
except RequestRejectedException as ex:
|
|
524
522
|
if ex.message == 'ILLEGAL DATA ADDRESS':
|
|
525
|
-
logger.warning("
|
|
523
|
+
logger.warning("Cannot read battery 2 values, disabling further attempts.")
|
|
526
524
|
self._has_battery2 = False
|
|
527
525
|
else:
|
|
528
526
|
raise ex
|
|
@@ -533,7 +531,7 @@ class ET(Inverter):
|
|
|
533
531
|
data.update(self._map_response(response, self._sensors_meter))
|
|
534
532
|
except RequestRejectedException as ex:
|
|
535
533
|
if ex.message == 'ILLEGAL DATA ADDRESS':
|
|
536
|
-
logger.warning("
|
|
534
|
+
logger.warning("Cannot read extended meter values, disabling further attempts.")
|
|
537
535
|
self._has_meter_extended = False
|
|
538
536
|
self._sensors_meter = tuple(filter(self._not_extended_meter, self._sensors_meter))
|
|
539
537
|
response = await self._read_from_socket(self._READ_METER_DATA)
|
|
@@ -551,7 +549,7 @@ class ET(Inverter):
|
|
|
551
549
|
data.update(self._map_response(response, self._sensors_mppt))
|
|
552
550
|
except RequestRejectedException as ex:
|
|
553
551
|
if ex.message == 'ILLEGAL DATA ADDRESS':
|
|
554
|
-
logger.warning("MPPT values
|
|
552
|
+
logger.warning("Cannot read MPPT values, disabling further attempts.")
|
|
555
553
|
self._has_mppt = False
|
|
556
554
|
else:
|
|
557
555
|
raise ex
|
|
@@ -566,7 +564,7 @@ class ET(Inverter):
|
|
|
566
564
|
|
|
567
565
|
async def _read_setting(self, setting: Sensor) -> Any:
|
|
568
566
|
count = (setting.size_ + (setting.size_ % 2)) // 2
|
|
569
|
-
response = await self._read_from_socket(
|
|
567
|
+
response = await self._read_from_socket(self._read_command(setting.offset, count))
|
|
570
568
|
return setting.read_value(response)
|
|
571
569
|
|
|
572
570
|
async def write_setting(self, setting_id: str, value: Any):
|
|
@@ -578,15 +576,15 @@ class ET(Inverter):
|
|
|
578
576
|
async def _write_setting(self, setting: Sensor, value: Any):
|
|
579
577
|
if setting.size_ == 1:
|
|
580
578
|
# modbus can address/store only 16 bit values, read the other 8 bytes
|
|
581
|
-
response = await self._read_from_socket(
|
|
579
|
+
response = await self._read_from_socket(self._read_command(setting.offset, 1))
|
|
582
580
|
raw_value = setting.encode_value(value, response.response_data()[0:2])
|
|
583
581
|
else:
|
|
584
582
|
raw_value = setting.encode_value(value)
|
|
585
583
|
if len(raw_value) <= 2:
|
|
586
584
|
value = int.from_bytes(raw_value, byteorder="big", signed=True)
|
|
587
|
-
await self._read_from_socket(
|
|
585
|
+
await self._read_from_socket(self._write_command(setting.offset, value))
|
|
588
586
|
else:
|
|
589
|
-
await self._read_from_socket(
|
|
587
|
+
await self._read_from_socket(self._write_multi_command(setting.offset, raw_value))
|
|
590
588
|
|
|
591
589
|
async def read_settings_data(self) -> Dict[str, Any]:
|
|
592
590
|
data = {}
|
|
@@ -617,7 +615,7 @@ class ET(Inverter):
|
|
|
617
615
|
result.remove(OperationMode.ECO_DISCHARGE)
|
|
618
616
|
return tuple(result)
|
|
619
617
|
|
|
620
|
-
async def get_operation_mode(self) -> OperationMode:
|
|
618
|
+
async def get_operation_mode(self) -> OperationMode | None:
|
|
621
619
|
mode_id = await self.read_setting('work_mode')
|
|
622
620
|
try:
|
|
623
621
|
mode = OperationMode(mode_id)
|
|
@@ -705,8 +703,8 @@ class ET(Inverter):
|
|
|
705
703
|
return tuple(self._settings.values())
|
|
706
704
|
|
|
707
705
|
async def _clear_battery_mode_param(self) -> None:
|
|
708
|
-
await self._read_from_socket(
|
|
706
|
+
await self._read_from_socket(self._write_command(0xb9ad, 1))
|
|
709
707
|
|
|
710
708
|
async def _set_offline(self, mode: bool) -> None:
|
|
711
709
|
value = bytes.fromhex('00070000') if mode else bytes.fromhex('00010000')
|
|
712
|
-
await self._read_from_socket(
|
|
710
|
+
await self._read_from_socket(self._write_multi_command(0xb997, value))
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import asyncio
|
|
4
3
|
import logging
|
|
5
4
|
from abc import ABC, abstractmethod
|
|
6
5
|
from dataclasses import dataclass
|
|
@@ -8,7 +7,7 @@ from enum import Enum, IntEnum
|
|
|
8
7
|
from typing import Any, Callable, Dict, Tuple, Optional
|
|
9
8
|
|
|
10
9
|
from .exceptions import MaxRetriesException, RequestFailedException
|
|
11
|
-
from .protocol import ProtocolCommand, ProtocolResponse
|
|
10
|
+
from .protocol import InverterProtocol, ProtocolCommand, ProtocolResponse, TcpInverterProtocol, UdpInverterProtocol
|
|
12
11
|
|
|
13
12
|
logger = logging.getLogger(__name__)
|
|
14
13
|
|
|
@@ -87,15 +86,12 @@ class Inverter(ABC):
|
|
|
87
86
|
Represents the inverter state and its basic behavior
|
|
88
87
|
"""
|
|
89
88
|
|
|
90
|
-
def __init__(self, host: str, comm_addr: int = 0, timeout: int = 1, retries: int = 3):
|
|
91
|
-
self.
|
|
92
|
-
self.comm_addr: int = comm_addr
|
|
93
|
-
self.timeout: int = timeout
|
|
94
|
-
self.retries: int = retries
|
|
95
|
-
self._running_loop: asyncio.AbstractEventLoop | None = None
|
|
96
|
-
self._lock: asyncio.Lock | None = None
|
|
89
|
+
def __init__(self, host: str, port: int, comm_addr: int = 0, timeout: int = 1, retries: int = 3):
|
|
90
|
+
self._protocol: InverterProtocol = self._create_protocol(host, port, timeout, retries)
|
|
97
91
|
self._consecutive_failures_count: int = 0
|
|
98
92
|
|
|
93
|
+
self.comm_addr: int = comm_addr
|
|
94
|
+
|
|
99
95
|
self.model_name: str | None = None
|
|
100
96
|
self.serial_number: str | None = None
|
|
101
97
|
self.rated_power: int = 0
|
|
@@ -109,36 +105,30 @@ class Inverter(ABC):
|
|
|
109
105
|
self.arm_version: int = 0
|
|
110
106
|
self.arm_svn_version: int | None = None
|
|
111
107
|
|
|
112
|
-
def
|
|
113
|
-
"""
|
|
108
|
+
def _read_command(self, offset: int, count: int) -> ProtocolCommand:
|
|
109
|
+
"""Create read protocol command."""
|
|
110
|
+
return self._protocol.read_command(self.comm_addr, offset, count)
|
|
114
111
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
"""
|
|
121
|
-
|
|
122
|
-
return self._lock
|
|
123
|
-
else:
|
|
124
|
-
logger.debug("Creating lock instance for current event loop.")
|
|
125
|
-
self._lock = asyncio.Lock()
|
|
126
|
-
self._running_loop = asyncio.get_event_loop()
|
|
127
|
-
return self._lock
|
|
112
|
+
def _write_command(self, register: int, value: int) -> ProtocolCommand:
|
|
113
|
+
"""Create write protocol command."""
|
|
114
|
+
return self._protocol.write_command(self.comm_addr, register, value)
|
|
115
|
+
|
|
116
|
+
def _write_multi_command(self, offset: int, values: bytes) -> ProtocolCommand:
|
|
117
|
+
"""Create write multiple protocol command."""
|
|
118
|
+
return self._protocol.write_multi_command(self.comm_addr, offset, values)
|
|
128
119
|
|
|
129
120
|
async def _read_from_socket(self, command: ProtocolCommand) -> ProtocolResponse:
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
raise RequestFailedException(ex.message, self._consecutive_failures_count)
|
|
121
|
+
try:
|
|
122
|
+
result = await command.execute(self._protocol)
|
|
123
|
+
self._consecutive_failures_count = 0
|
|
124
|
+
return result
|
|
125
|
+
except MaxRetriesException:
|
|
126
|
+
self._consecutive_failures_count += 1
|
|
127
|
+
raise RequestFailedException(f'No valid response received even after {self._protocol.retries} retries',
|
|
128
|
+
self._consecutive_failures_count) from None
|
|
129
|
+
except RequestFailedException as ex:
|
|
130
|
+
self._consecutive_failures_count += 1
|
|
131
|
+
raise RequestFailedException(ex.message, self._consecutive_failures_count) from None
|
|
142
132
|
|
|
143
133
|
@abstractmethod
|
|
144
134
|
async def read_device_info(self):
|
|
@@ -190,8 +180,8 @@ class Inverter(ABC):
|
|
|
190
180
|
self, command: bytes, validator: Callable[[bytes], bool] = lambda x: True
|
|
191
181
|
) -> ProtocolResponse:
|
|
192
182
|
"""
|
|
193
|
-
Send low level
|
|
194
|
-
Answer command's raw response data.
|
|
183
|
+
Send low level command (as bytes).
|
|
184
|
+
Answer ProtocolResponse with command's raw response data.
|
|
195
185
|
"""
|
|
196
186
|
return await self._read_from_socket(ProtocolCommand(command, validator))
|
|
197
187
|
|
|
@@ -277,6 +267,13 @@ class Inverter(ABC):
|
|
|
277
267
|
"""
|
|
278
268
|
raise NotImplementedError()
|
|
279
269
|
|
|
270
|
+
@staticmethod
|
|
271
|
+
def _create_protocol(host: str, port: int, timeout: int, retries: int) -> InverterProtocol:
|
|
272
|
+
if port == 502:
|
|
273
|
+
return TcpInverterProtocol(host, port, timeout, retries)
|
|
274
|
+
else:
|
|
275
|
+
return UdpInverterProtocol(host, port, timeout, retries)
|
|
276
|
+
|
|
280
277
|
@staticmethod
|
|
281
278
|
def _map_response(response: ProtocolResponse, sensors: Tuple[Sensor, ...]) -> Dict[str, Any]:
|
|
282
279
|
"""Process the response data and return dictionary with runtime values"""
|