goodwe 0.3.6__py3-none-any.whl → 0.4.1__py3-none-any.whl
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/__init__.py +32 -37
- goodwe/const.py +1 -0
- goodwe/dt.py +8 -12
- goodwe/es.py +14 -14
- goodwe/et.py +104 -79
- goodwe/inverter.py +37 -38
- goodwe/modbus.py +109 -7
- goodwe/model.py +0 -4
- goodwe/protocol.py +338 -58
- goodwe/sensor.py +32 -24
- {goodwe-0.3.6.dist-info → goodwe-0.4.1.dist-info}/METADATA +12 -7
- goodwe-0.4.1.dist-info/RECORD +16 -0
- goodwe-0.3.6.dist-info/RECORD +0 -16
- {goodwe-0.3.6.dist-info → goodwe-0.4.1.dist-info}/LICENSE +0 -0
- {goodwe-0.3.6.dist-info → goodwe-0.4.1.dist-info}/WHEEL +0 -0
- {goodwe-0.3.6.dist-info → goodwe-0.4.1.dist-info}/top_level.txt +0 -0
goodwe/__init__.py
CHANGED
|
@@ -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()
|
goodwe/const.py
CHANGED
goodwe/dt.py
CHANGED
|
@@ -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
|
|
|
@@ -110,10 +110,6 @@ class DT(Inverter):
|
|
|
110
110
|
Integer("shadow_scan", 40326, "Shadow Scan", "", Kind.PV),
|
|
111
111
|
Integer("grid_export", 40327, "Grid Export Enabled", "", Kind.GRID),
|
|
112
112
|
Integer("grid_export_limit", 40328, "Grid Export Limit", "%", Kind.GRID),
|
|
113
|
-
Integer("start", 40330, "Start / Power On", "", Kind.GRID),
|
|
114
|
-
Integer("stop", 40331, "Stop / Power Off", "", Kind.GRID),
|
|
115
|
-
Integer("restart", 40332, "Restart", "", Kind.GRID),
|
|
116
|
-
Integer("grid_export_hw", 40345, "Grid Export Enabled (HW)", "", Kind.GRID),
|
|
117
113
|
)
|
|
118
114
|
|
|
119
115
|
# Settings for single phase inverters
|
|
@@ -126,13 +122,13 @@ class DT(Inverter):
|
|
|
126
122
|
Integer("grid_export_limit", 40336, "Grid Export Limit", "%", Kind.GRID),
|
|
127
123
|
)
|
|
128
124
|
|
|
129
|
-
def __init__(self, host: str, comm_addr: int = 0, timeout: int = 1, retries: int = 3):
|
|
130
|
-
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)
|
|
131
127
|
if not self.comm_addr:
|
|
132
128
|
# Set the default inverter address
|
|
133
129
|
self.comm_addr = 0x7f
|
|
134
|
-
self._READ_DEVICE_VERSION_INFO: ProtocolCommand =
|
|
135
|
-
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)
|
|
136
132
|
self._sensors = self.__all_sensors
|
|
137
133
|
self._settings: dict[str, Sensor] = {s.id_: s for s in self.__all_settings}
|
|
138
134
|
|
|
@@ -184,7 +180,7 @@ class DT(Inverter):
|
|
|
184
180
|
if not setting:
|
|
185
181
|
raise ValueError(f'Unknown setting "{setting_id}"')
|
|
186
182
|
count = (setting.size_ + (setting.size_ % 2)) // 2
|
|
187
|
-
response = await self._read_from_socket(
|
|
183
|
+
response = await self._read_from_socket(self._read_command(setting.offset, count))
|
|
188
184
|
return setting.read_value(response)
|
|
189
185
|
|
|
190
186
|
async def write_setting(self, setting_id: str, value: Any):
|
|
@@ -194,9 +190,9 @@ class DT(Inverter):
|
|
|
194
190
|
raw_value = setting.encode_value(value)
|
|
195
191
|
if len(raw_value) <= 2:
|
|
196
192
|
value = int.from_bytes(raw_value, byteorder="big", signed=True)
|
|
197
|
-
await self._read_from_socket(
|
|
193
|
+
await self._read_from_socket(self._write_command(setting.offset, value))
|
|
198
194
|
else:
|
|
199
|
-
await self._read_from_socket(
|
|
195
|
+
await self._read_from_socket(self._write_multi_command(setting.offset, raw_value))
|
|
200
196
|
|
|
201
197
|
async def read_settings_data(self) -> Dict[str, Any]:
|
|
202
198
|
data = {}
|
goodwe/es.py
CHANGED
|
@@ -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
|
|
@@ -178,11 +177,11 @@ class ES(Inverter):
|
|
|
178
177
|
def _supports_eco_mode_v2(self) -> bool:
|
|
179
178
|
if self.arm_version < 14:
|
|
180
179
|
return False
|
|
181
|
-
if "EMU" in self.serial_number
|
|
180
|
+
if "EMU" in self.serial_number:
|
|
182
181
|
return self.dsp1_version >= 11
|
|
183
|
-
if "ESU" in self.serial_number
|
|
182
|
+
if "ESU" in self.serial_number:
|
|
184
183
|
return self.dsp1_version >= 22
|
|
185
|
-
if "BPS" in self.serial_number
|
|
184
|
+
if "BPS" in self.serial_number:
|
|
186
185
|
return self.dsp1_version >= 10
|
|
187
186
|
return False
|
|
188
187
|
|
|
@@ -192,7 +191,7 @@ class ES(Inverter):
|
|
|
192
191
|
self.firmware = self._decode(response[0:5]).rstrip()
|
|
193
192
|
self.model_name = self._decode(response[5:15]).rstrip()
|
|
194
193
|
self.serial_number = self._decode(response[31:47])
|
|
195
|
-
self.
|
|
194
|
+
self.software_version = self._decode(response[51:63])
|
|
196
195
|
try:
|
|
197
196
|
if len(self.firmware) >= 2:
|
|
198
197
|
self.dsp1_version = int(self.firmware[0:2])
|
|
@@ -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,21 +248,22 @@ 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))
|
|
252
|
+
raw_value = setting.encode_value(value, response.response_data()[0:2])
|
|
253
253
|
else:
|
|
254
254
|
response = await self._read_from_socket(Aa55ReadCommand(setting.offset, 1))
|
|
255
|
-
|
|
255
|
+
raw_value = setting.encode_value(value, response.response_data()[2:4])
|
|
256
256
|
else:
|
|
257
257
|
raw_value = setting.encode_value(value)
|
|
258
258
|
if len(raw_value) <= 2:
|
|
259
259
|
value = int.from_bytes(raw_value, byteorder="big", signed=True)
|
|
260
260
|
if self._is_modbus_setting(setting):
|
|
261
|
-
await self._read_from_socket(
|
|
261
|
+
await self._read_from_socket(self._write_command(setting.offset, value))
|
|
262
262
|
else:
|
|
263
263
|
await self._read_from_socket(Aa55WriteCommand(setting.offset, value))
|
|
264
264
|
else:
|
|
265
265
|
if self._is_modbus_setting(setting):
|
|
266
|
-
await self._read_from_socket(
|
|
266
|
+
await self._read_from_socket(self._write_multi_command(setting.offset, raw_value))
|
|
267
267
|
else:
|
|
268
268
|
await self._read_from_socket(Aa55WriteMultiCommand(setting.offset, raw_value))
|
|
269
269
|
|
|
@@ -290,7 +290,7 @@ class ES(Inverter):
|
|
|
290
290
|
result.remove(OperationMode.ECO_DISCHARGE)
|
|
291
291
|
return tuple(result)
|
|
292
292
|
|
|
293
|
-
async def get_operation_mode(self) -> OperationMode:
|
|
293
|
+
async def get_operation_mode(self) -> OperationMode | None:
|
|
294
294
|
mode_id = await self.read_setting('work_mode')
|
|
295
295
|
try:
|
|
296
296
|
mode = OperationMode(mode_id)
|
goodwe/et.py
CHANGED
|
@@ -7,8 +7,9 @@ from .exceptions import RequestFailedException, RequestRejectedException
|
|
|
7
7
|
from .inverter import Inverter
|
|
8
8
|
from .inverter import OperationMode
|
|
9
9
|
from .inverter import SensorKind as Kind
|
|
10
|
+
from .modbus import ILLEGAL_DATA_ADDRESS
|
|
10
11
|
from .model import is_2_battery, is_4_mppt, is_745_platform, is_single_phase
|
|
11
|
-
from .protocol import ProtocolCommand
|
|
12
|
+
from .protocol import ProtocolCommand
|
|
12
13
|
from .sensor import *
|
|
13
14
|
|
|
14
15
|
logger = logging.getLogger(__name__)
|
|
@@ -152,6 +153,10 @@ class ET(Inverter):
|
|
|
152
153
|
read_bytes4_signed(data, 35182) -
|
|
153
154
|
read_bytes2_signed(data, 35140),
|
|
154
155
|
"House Consumption", "W", Kind.AC),
|
|
156
|
+
|
|
157
|
+
# Power4S("pbattery2", 35264, "Battery2 Power", Kind.BAT),
|
|
158
|
+
# Integer("battery2_mode", 35266, "Battery2 Mode code", "", Kind.BAT),
|
|
159
|
+
# Enum2("battery2_mode_label", 35184, BATTERY_MODES, "Battery2 Mode", Kind.BAT),
|
|
155
160
|
)
|
|
156
161
|
|
|
157
162
|
# Modbus registers from offset 0x9088 (37000)
|
|
@@ -252,8 +257,7 @@ class ET(Inverter):
|
|
|
252
257
|
Apparent4("meter_apparent_power_total", 36041, "Meter Apparent Power Total", Kind.GRID),
|
|
253
258
|
Integer("meter_type", 36043, "Meter Type", "", Kind.GRID), # (0: Single phase, 1: 3P3W, 2: 3P4W, 3: HomeKit)
|
|
254
259
|
Integer("meter_sw_version", 36044, "Meter Software Version", "", Kind.GRID),
|
|
255
|
-
|
|
256
|
-
# Sensors added in some ARM fw update (or platform 745/753), read when flag _has_meter_extended is on
|
|
260
|
+
# Sensors added in some ARM fw update, read when flag _has_meter_extended is on
|
|
257
261
|
Power4S("meter2_active_power", 36045, "Meter 2 Active Power", Kind.GRID),
|
|
258
262
|
Float("meter2_e_total_exp", 36047, 1000, "Meter 2 Total Energy (export)", "kWh", Kind.GRID),
|
|
259
263
|
Float("meter2_e_total_imp", 36049, 1000, "Meter 2 Total Energy (import)", "kWh", Kind.GRID),
|
|
@@ -264,15 +268,6 @@ class ET(Inverter):
|
|
|
264
268
|
Current("meter_current1", 36055, "Meter L1 Current", Kind.GRID),
|
|
265
269
|
Current("meter_current2", 36056, "Meter L2 Current", Kind.GRID),
|
|
266
270
|
Current("meter_current3", 36057, "Meter L3 Current", Kind.GRID),
|
|
267
|
-
|
|
268
|
-
Energy8("meter_e_total_exp1", 36092, "Meter Total Energy (export) L1", Kind.GRID),
|
|
269
|
-
Energy8("meter_e_total_exp2", 36096, "Meter Total Energy (export) L2", Kind.GRID),
|
|
270
|
-
Energy8("meter_e_total_exp3", 36100, "Meter Total Energy (export) L3", Kind.GRID),
|
|
271
|
-
Energy8("meter_e_total_exp", 36104, "Meter Total Energy (export)", Kind.GRID),
|
|
272
|
-
Energy8("meter_e_total_imp1", 36108, "Meter Total Energy (import) L1", Kind.GRID),
|
|
273
|
-
Energy8("meter_e_total_imp2", 36112, "Meter Total Energy (import) L2", Kind.GRID),
|
|
274
|
-
Energy8("meter_e_total_imp3", 36116, "Meter Total Energy (import) L3", Kind.GRID),
|
|
275
|
-
Energy8("meter_e_total_imp", 36120, "Meter Total Energy (import)", Kind.GRID),
|
|
276
271
|
)
|
|
277
272
|
|
|
278
273
|
# Inverter's MPPT data
|
|
@@ -337,7 +332,7 @@ class ET(Inverter):
|
|
|
337
332
|
# Modbus registers of inverter settings, offsets are modbus register addresses
|
|
338
333
|
__all_settings: Tuple[Sensor, ...] = (
|
|
339
334
|
Integer("comm_address", 45127, "Communication Address", ""),
|
|
340
|
-
|
|
335
|
+
Integer("modbus_baud_rate", 45132, "Modbus Baud rate", ""),
|
|
341
336
|
Timestamp("time", 45200, "Inverter time"),
|
|
342
337
|
|
|
343
338
|
Integer("sensitivity_check", 45246, "Sensitivity Check Mode", "", Kind.AC),
|
|
@@ -377,6 +372,51 @@ class ET(Inverter):
|
|
|
377
372
|
ByteH("eco_mode_3_switch", 47526, "Eco Mode Group 3 Switch"),
|
|
378
373
|
EcoModeV1("eco_mode_4", 47527, "Eco Mode Group 4"),
|
|
379
374
|
ByteH("eco_mode_4_switch", 47530, "Eco Mode Group 4 Switch"),
|
|
375
|
+
|
|
376
|
+
# Direct BMS communication for EMS Control
|
|
377
|
+
Integer("bms_version", 47900, "BMS Version"),
|
|
378
|
+
Integer("bms_bat_modules", 47901, "BMS Battery Modules"),
|
|
379
|
+
# Real time read from BMS
|
|
380
|
+
Voltage("bms_bat_charge_v_max", 47902, "BMS Battery Charge Voltage (max)", Kind.BMS),
|
|
381
|
+
Current("bms_bat_charge_i_max", 47903, "BMS Battery Charge Current (max)", Kind.BMS),
|
|
382
|
+
Voltage("bms_bat_discharge_v_min", 47904, "BMS min. Battery Discharge Voltage (min)", Kind.BMS),
|
|
383
|
+
Current("bms_bat_discharge_i_max", 47905, "BMS max. Battery Discharge Current (max)", Kind.BMS),
|
|
384
|
+
Voltage("bms_bat_voltage", 47906, "BMS Battery Voltage", Kind.BMS),
|
|
385
|
+
Current("bms_bat_current", 47907, "BMS Battery Current", Kind.BMS),
|
|
386
|
+
#
|
|
387
|
+
Integer("bms_bat_soc", 47908, "BMS Battery State of Charge", "%", Kind.BMS),
|
|
388
|
+
Integer("bms_bat_soh", 47909, "BMS Battery State of Health", "%", Kind.BMS),
|
|
389
|
+
Temp("bms_bat_temperature", 47910, "BMS Battery Temperature", Kind.BMS),
|
|
390
|
+
Long("bms_bat_warning-code", 47911, "BMS Battery Warning Code"),
|
|
391
|
+
# Reserved
|
|
392
|
+
Long("bms_bat_alarm-code", 47913, "BMS Battery Alarm Code"),
|
|
393
|
+
Integer("bms_status", 47915, "BMS Status"),
|
|
394
|
+
Integer("bms_comm_loss_disable", 47916, "BMS Communication Loss Disable"),
|
|
395
|
+
# RW settings of BMS voltage rate
|
|
396
|
+
Integer("bms_battery_string_rate_v", 47917, "BMS Battery String Rate Voltage"),
|
|
397
|
+
|
|
398
|
+
# Direct BMS communication for EMS Control
|
|
399
|
+
Integer("bms2_version", 47918, "BMS2 Version"),
|
|
400
|
+
Integer("bms2_bat_modules", 47919, "BMS2 Battery Modules"),
|
|
401
|
+
# Real time read from BMS
|
|
402
|
+
Voltage("bms2_bat_charge_v_max", 47920, "BMS2 Battery Charge Voltage (max)", Kind.BMS),
|
|
403
|
+
Current("bms2_bat_charge_i_max", 47921, "BMS2 Battery Charge Current (max)", Kind.BMS),
|
|
404
|
+
Voltage("bms2_bat_discharge_v_min", 47922, "BMS2 min. Battery Discharge Voltage (min)", Kind.BMS),
|
|
405
|
+
Current("bms2_bat_discharge_i_max", 47923, "BMS2 max. Battery Discharge Current (max)", Kind.BMS),
|
|
406
|
+
Voltage("bms2_bat_voltage", 47924, "BMS2 Battery Voltage", Kind.BMS),
|
|
407
|
+
Current("bms2_bat_current", 47925, "BMS2 Battery Current", Kind.BMS),
|
|
408
|
+
#
|
|
409
|
+
Integer("bms2_bat_soc", 47926, "BMS2 Battery State of Charge", "%", Kind.BMS),
|
|
410
|
+
Integer("bms2_bat_soh", 47927, "BMS2 Battery State of Health", "%", Kind.BMS),
|
|
411
|
+
Temp("bms2_bat_temperature", 47928, "BMS2 Battery Temperature", Kind.BMS),
|
|
412
|
+
Long("bms2_bat_warning-code", 47929, "BMS2 Battery Warning Code"),
|
|
413
|
+
# Reserved
|
|
414
|
+
Long("bms2_bat_alarm-code", 47931, "BMS2 Battery Alarm Code"),
|
|
415
|
+
Integer("bms2_status", 47933, "BMS2 Status"),
|
|
416
|
+
Integer("bms2_comm_loss_disable", 47934, "BMS2 Communication Loss Disable"),
|
|
417
|
+
# RW settings of BMS voltage rate
|
|
418
|
+
Integer("bms2_battery_string_rate_v", 47935, "BMS2 Battery String Rate Voltage"),
|
|
419
|
+
|
|
380
420
|
)
|
|
381
421
|
|
|
382
422
|
# Settings added in ARM firmware 19
|
|
@@ -395,6 +435,7 @@ class ET(Inverter):
|
|
|
395
435
|
Integer("load_control_mode", 47595, "Load Control Mode", "", Kind.AC),
|
|
396
436
|
Integer("load_control_switch", 47596, "Load Control Switch", "", Kind.AC),
|
|
397
437
|
Integer("load_control_soc", 47597, "Load Control SoC", "", Kind.AC),
|
|
438
|
+
Integer("hardware_feed_power", 47599, "Hardware Feed Power"),
|
|
398
439
|
|
|
399
440
|
Integer("fast_charging_power", 47603, "Fast Charging Power", "%", Kind.BAT),
|
|
400
441
|
)
|
|
@@ -415,25 +456,23 @@ class ET(Inverter):
|
|
|
415
456
|
Integer("eco_mode_enable", 47612, "Eco Mode Switch"),
|
|
416
457
|
)
|
|
417
458
|
|
|
418
|
-
def __init__(self, host: str, comm_addr: int = 0, timeout: int = 1, retries: int = 3):
|
|
419
|
-
super().__init__(host, comm_addr, timeout, retries)
|
|
459
|
+
def __init__(self, host: str, port: int, comm_addr: int = 0, timeout: int = 1, retries: int = 3):
|
|
460
|
+
super().__init__(host, port, comm_addr, timeout, retries)
|
|
420
461
|
if not self.comm_addr:
|
|
421
462
|
# Set the default inverter address
|
|
422
463
|
self.comm_addr = 0xf7
|
|
423
|
-
self._READ_DEVICE_VERSION_INFO: ProtocolCommand =
|
|
424
|
-
self._READ_RUNNING_DATA: ProtocolCommand =
|
|
425
|
-
self._READ_METER_DATA: ProtocolCommand =
|
|
426
|
-
self._READ_METER_DATA_EXTENDED: ProtocolCommand =
|
|
427
|
-
self.
|
|
428
|
-
self.
|
|
429
|
-
self.
|
|
430
|
-
self._READ_MPPT_DATA: ProtocolCommand = ModbusReadCommand(self.comm_addr, 0x89e5, 0x3d)
|
|
464
|
+
self._READ_DEVICE_VERSION_INFO: ProtocolCommand = self._read_command(0x88b8, 0x0021)
|
|
465
|
+
self._READ_RUNNING_DATA: ProtocolCommand = self._read_command(0x891c, 0x007d)
|
|
466
|
+
self._READ_METER_DATA: ProtocolCommand = self._read_command(0x8ca0, 0x2d)
|
|
467
|
+
self._READ_METER_DATA_EXTENDED: ProtocolCommand = self._read_command(0x8ca0, 0x3a)
|
|
468
|
+
self._READ_BATTERY_INFO: ProtocolCommand = self._read_command(0x9088, 0x0018)
|
|
469
|
+
self._READ_BATTERY2_INFO: ProtocolCommand = self._read_command(0x9858, 0x0016)
|
|
470
|
+
self._READ_MPPT_DATA: ProtocolCommand = self._read_command(0x89e5, 0x3d)
|
|
431
471
|
self._has_eco_mode_v2: bool = True
|
|
432
472
|
self._has_peak_shaving: bool = True
|
|
433
473
|
self._has_battery: bool = True
|
|
434
474
|
self._has_battery2: bool = False
|
|
435
475
|
self._has_meter_extended: bool = False
|
|
436
|
-
self._has_meter_extended2: bool = False
|
|
437
476
|
self._has_mppt: bool = False
|
|
438
477
|
self._sensors = self.__all_sensors
|
|
439
478
|
self._sensors_battery = self.__all_sensors_battery
|
|
@@ -452,27 +491,22 @@ class ET(Inverter):
|
|
|
452
491
|
"""Filter to exclude extended meter sensors"""
|
|
453
492
|
return s.offset < 36045
|
|
454
493
|
|
|
455
|
-
@staticmethod
|
|
456
|
-
def _not_extended_meter2(s: Sensor) -> bool:
|
|
457
|
-
"""Filter to exclude extended meter sensors"""
|
|
458
|
-
return s.offset < 36058
|
|
459
|
-
|
|
460
494
|
async def read_device_info(self):
|
|
461
495
|
response = await self._read_from_socket(self._READ_DEVICE_VERSION_INFO)
|
|
462
496
|
response = response.response_data()
|
|
463
|
-
# Modbus registers from
|
|
497
|
+
# Modbus registers from 35000 - 35032
|
|
464
498
|
self.modbus_version = read_unsigned_int(response, 0)
|
|
465
499
|
self.rated_power = read_unsigned_int(response, 2)
|
|
466
500
|
self.ac_output_type = read_unsigned_int(response, 4) # 0: 1-phase, 1: 3-phase (4 wire), 2: 3-phase (3 wire)
|
|
467
|
-
self.serial_number = self._decode(response[6:22])
|
|
468
|
-
self.model_name = self._decode(response[22:32])
|
|
469
|
-
self.dsp1_version = read_unsigned_int(response, 32)
|
|
470
|
-
self.dsp2_version = read_unsigned_int(response, 34)
|
|
471
|
-
self.dsp_svn_version = read_unsigned_int(response, 36)
|
|
472
|
-
self.arm_version = read_unsigned_int(response, 38)
|
|
473
|
-
self.arm_svn_version = read_unsigned_int(response, 40)
|
|
474
|
-
self.firmware = self._decode(response[42:54])
|
|
475
|
-
self.arm_firmware = self._decode(response[54:66])
|
|
501
|
+
self.serial_number = self._decode(response[6:22]) # 35003 - 350010
|
|
502
|
+
self.model_name = self._decode(response[22:32]) # 35011 - 35015
|
|
503
|
+
self.dsp1_version = read_unsigned_int(response, 32) # 35016
|
|
504
|
+
self.dsp2_version = read_unsigned_int(response, 34) # 35017
|
|
505
|
+
self.dsp_svn_version = read_unsigned_int(response, 36) # 35018
|
|
506
|
+
self.arm_version = read_unsigned_int(response, 38) # 35019
|
|
507
|
+
self.arm_svn_version = read_unsigned_int(response, 40) # 35020
|
|
508
|
+
self.firmware = self._decode(response[42:54]) # 35021 - 35027
|
|
509
|
+
self.arm_firmware = self._decode(response[54:66]) # 35027 - 35032
|
|
476
510
|
|
|
477
511
|
if not is_4_mppt(self) and self.rated_power < 15000:
|
|
478
512
|
# This inverter does not have 4 MPPTs or PV strings
|
|
@@ -487,34 +521,33 @@ class ET(Inverter):
|
|
|
487
521
|
if is_2_battery(self) or self.rated_power >= 25000:
|
|
488
522
|
self._has_battery2 = True
|
|
489
523
|
|
|
490
|
-
if
|
|
524
|
+
if self.rated_power >= 15000:
|
|
491
525
|
self._has_mppt = True
|
|
492
526
|
self._has_meter_extended = True
|
|
493
|
-
self._has_meter_extended2 = True
|
|
494
527
|
else:
|
|
495
528
|
self._sensors_meter = tuple(filter(self._not_extended_meter, self._sensors_meter))
|
|
496
529
|
|
|
497
530
|
# Check and add EcoModeV2 settings added in (ETU fw 19)
|
|
498
531
|
try:
|
|
499
|
-
await self._read_from_socket(
|
|
532
|
+
await self._read_from_socket(self._read_command(47547, 6))
|
|
500
533
|
self._settings.update({s.id_: s for s in self.__settings_arm_fw_19})
|
|
501
534
|
except RequestRejectedException as ex:
|
|
502
|
-
if ex.message ==
|
|
535
|
+
if ex.message == ILLEGAL_DATA_ADDRESS:
|
|
503
536
|
logger.debug("EcoModeV2 settings not supported, switching to EcoModeV1.")
|
|
504
537
|
self._has_eco_mode_v2 = False
|
|
505
|
-
except RequestFailedException
|
|
538
|
+
except RequestFailedException:
|
|
506
539
|
logger.debug("Cannot read EcoModeV2 settings, switching to EcoModeV1.")
|
|
507
540
|
self._has_eco_mode_v2 = False
|
|
508
541
|
|
|
509
542
|
# Check and add Peak Shaving settings added in (ETU fw 22)
|
|
510
543
|
try:
|
|
511
|
-
await self._read_from_socket(
|
|
544
|
+
await self._read_from_socket(self._read_command(47589, 6))
|
|
512
545
|
self._settings.update({s.id_: s for s in self.__settings_arm_fw_22})
|
|
513
546
|
except RequestRejectedException as ex:
|
|
514
|
-
if ex.message ==
|
|
547
|
+
if ex.message == ILLEGAL_DATA_ADDRESS:
|
|
515
548
|
logger.debug("PeakShaving setting not supported, disabling it.")
|
|
516
549
|
self._has_peak_shaving = False
|
|
517
|
-
except RequestFailedException
|
|
550
|
+
except RequestFailedException:
|
|
518
551
|
logger.debug("Cannot read _has_peak_shaving settings, disabling it.")
|
|
519
552
|
self._has_peak_shaving = False
|
|
520
553
|
|
|
@@ -528,8 +561,8 @@ class ET(Inverter):
|
|
|
528
561
|
response = await self._read_from_socket(self._READ_BATTERY_INFO)
|
|
529
562
|
data.update(self._map_response(response, self._sensors_battery))
|
|
530
563
|
except RequestRejectedException as ex:
|
|
531
|
-
if ex.message ==
|
|
532
|
-
logger.
|
|
564
|
+
if ex.message == ILLEGAL_DATA_ADDRESS:
|
|
565
|
+
logger.info("Battery values not supported, disabling further attempts.")
|
|
533
566
|
self._has_battery = False
|
|
534
567
|
else:
|
|
535
568
|
raise ex
|
|
@@ -539,33 +572,19 @@ class ET(Inverter):
|
|
|
539
572
|
data.update(
|
|
540
573
|
self._map_response(response, self._sensors_battery2))
|
|
541
574
|
except RequestRejectedException as ex:
|
|
542
|
-
if ex.message ==
|
|
543
|
-
logger.
|
|
575
|
+
if ex.message == ILLEGAL_DATA_ADDRESS:
|
|
576
|
+
logger.info("Battery 2 values not supported, disabling further attempts.")
|
|
544
577
|
self._has_battery2 = False
|
|
545
578
|
else:
|
|
546
579
|
raise ex
|
|
547
580
|
|
|
548
|
-
if self.
|
|
549
|
-
try:
|
|
550
|
-
response = await self._read_from_socket(self._READ_METER_DATA_EXTENDED2)
|
|
551
|
-
data.update(self._map_response(response, self._sensors_meter))
|
|
552
|
-
except RequestRejectedException as ex:
|
|
553
|
-
if ex.message == 'ILLEGAL DATA ADDRESS':
|
|
554
|
-
logger.info("Extended meter values not supported, disabling further attempts.")
|
|
555
|
-
self._has_meter_extended2 = False
|
|
556
|
-
self._sensors_meter = tuple(filter(self._not_extended_meter2, self._sensors_meter))
|
|
557
|
-
response = await self._read_from_socket(self._READ_METER_DATA_EXTENDED)
|
|
558
|
-
data.update(
|
|
559
|
-
self._map_response(response, self._sensors_meter))
|
|
560
|
-
else:
|
|
561
|
-
raise ex
|
|
562
|
-
elif self._has_meter_extended:
|
|
581
|
+
if self._has_meter_extended:
|
|
563
582
|
try:
|
|
564
583
|
response = await self._read_from_socket(self._READ_METER_DATA_EXTENDED)
|
|
565
584
|
data.update(self._map_response(response, self._sensors_meter))
|
|
566
585
|
except RequestRejectedException as ex:
|
|
567
|
-
if ex.message ==
|
|
568
|
-
logger.
|
|
586
|
+
if ex.message == ILLEGAL_DATA_ADDRESS:
|
|
587
|
+
logger.info("Extended meter values not supported, disabling further attempts.")
|
|
569
588
|
self._has_meter_extended = False
|
|
570
589
|
self._sensors_meter = tuple(filter(self._not_extended_meter, self._sensors_meter))
|
|
571
590
|
response = await self._read_from_socket(self._READ_METER_DATA)
|
|
@@ -582,8 +601,8 @@ class ET(Inverter):
|
|
|
582
601
|
response = await self._read_from_socket(self._READ_MPPT_DATA)
|
|
583
602
|
data.update(self._map_response(response, self._sensors_mppt))
|
|
584
603
|
except RequestRejectedException as ex:
|
|
585
|
-
if ex.message ==
|
|
586
|
-
logger.
|
|
604
|
+
if ex.message == ILLEGAL_DATA_ADDRESS:
|
|
605
|
+
logger.info("MPPT values not supported, disabling further attempts.")
|
|
587
606
|
self._has_mppt = False
|
|
588
607
|
else:
|
|
589
608
|
raise ex
|
|
@@ -594,11 +613,17 @@ class ET(Inverter):
|
|
|
594
613
|
setting = self._settings.get(setting_id)
|
|
595
614
|
if not setting:
|
|
596
615
|
raise ValueError(f'Unknown setting "{setting_id}"')
|
|
597
|
-
|
|
616
|
+
try:
|
|
617
|
+
return await self._read_setting(setting)
|
|
618
|
+
except RequestRejectedException as ex:
|
|
619
|
+
if ex.message == ILLEGAL_DATA_ADDRESS:
|
|
620
|
+
logger.debug("Unsupported setting %s", setting.id_)
|
|
621
|
+
self._settings.pop(setting_id, None)
|
|
622
|
+
return None
|
|
598
623
|
|
|
599
624
|
async def _read_setting(self, setting: Sensor) -> Any:
|
|
600
625
|
count = (setting.size_ + (setting.size_ % 2)) // 2
|
|
601
|
-
response = await self._read_from_socket(
|
|
626
|
+
response = await self._read_from_socket(self._read_command(setting.offset, count))
|
|
602
627
|
return setting.read_value(response)
|
|
603
628
|
|
|
604
629
|
async def write_setting(self, setting_id: str, value: Any):
|
|
@@ -610,15 +635,15 @@ class ET(Inverter):
|
|
|
610
635
|
async def _write_setting(self, setting: Sensor, value: Any):
|
|
611
636
|
if setting.size_ == 1:
|
|
612
637
|
# modbus can address/store only 16 bit values, read the other 8 bytes
|
|
613
|
-
response = await self._read_from_socket(
|
|
638
|
+
response = await self._read_from_socket(self._read_command(setting.offset, 1))
|
|
614
639
|
raw_value = setting.encode_value(value, response.response_data()[0:2])
|
|
615
640
|
else:
|
|
616
641
|
raw_value = setting.encode_value(value)
|
|
617
642
|
if len(raw_value) <= 2:
|
|
618
643
|
value = int.from_bytes(raw_value, byteorder="big", signed=True)
|
|
619
|
-
await self._read_from_socket(
|
|
644
|
+
await self._read_from_socket(self._write_command(setting.offset, value))
|
|
620
645
|
else:
|
|
621
|
-
await self._read_from_socket(
|
|
646
|
+
await self._read_from_socket(self._write_multi_command(setting.offset, raw_value))
|
|
622
647
|
|
|
623
648
|
async def read_settings_data(self) -> Dict[str, Any]:
|
|
624
649
|
data = {}
|
|
@@ -626,7 +651,7 @@ class ET(Inverter):
|
|
|
626
651
|
try:
|
|
627
652
|
value = await self.read_setting(setting.id_)
|
|
628
653
|
data[setting.id_] = value
|
|
629
|
-
except ValueError:
|
|
654
|
+
except (ValueError, RequestFailedException):
|
|
630
655
|
logger.exception("Error reading setting %s.", setting.id_)
|
|
631
656
|
data[setting.id_] = None
|
|
632
657
|
return data
|
|
@@ -649,7 +674,7 @@ class ET(Inverter):
|
|
|
649
674
|
result.remove(OperationMode.ECO_DISCHARGE)
|
|
650
675
|
return tuple(result)
|
|
651
676
|
|
|
652
|
-
async def get_operation_mode(self) -> OperationMode:
|
|
677
|
+
async def get_operation_mode(self) -> OperationMode | None:
|
|
653
678
|
mode_id = await self.read_setting('work_mode')
|
|
654
679
|
try:
|
|
655
680
|
mode = OperationMode(mode_id)
|
|
@@ -737,8 +762,8 @@ class ET(Inverter):
|
|
|
737
762
|
return tuple(self._settings.values())
|
|
738
763
|
|
|
739
764
|
async def _clear_battery_mode_param(self) -> None:
|
|
740
|
-
await self._read_from_socket(
|
|
765
|
+
await self._read_from_socket(self._write_command(0xb9ad, 1))
|
|
741
766
|
|
|
742
767
|
async def _set_offline(self, mode: bool) -> None:
|
|
743
768
|
value = bytes.fromhex('00070000') if mode else bytes.fromhex('00010000')
|
|
744
|
-
await self._read_from_socket(
|
|
769
|
+
await self._read_from_socket(self._write_multi_command(0xb997, value))
|