goodwe 0.3.6__py3-none-any.whl → 0.4.0__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 +33 -67
- goodwe/inverter.py +35 -38
- goodwe/modbus.py +106 -6
- goodwe/model.py +0 -4
- goodwe/protocol.py +338 -58
- goodwe/sensor.py +7 -23
- {goodwe-0.3.6.dist-info → goodwe-0.4.0.dist-info}/METADATA +12 -7
- goodwe-0.4.0.dist-info/RECORD +16 -0
- goodwe-0.3.6.dist-info/RECORD +0 -16
- {goodwe-0.3.6.dist-info → goodwe-0.4.0.dist-info}/LICENSE +0 -0
- {goodwe-0.3.6.dist-info → goodwe-0.4.0.dist-info}/WHEEL +0 -0
- {goodwe-0.3.6.dist-info → goodwe-0.4.0.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
|
@@ -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)
|
|
@@ -252,8 +256,7 @@ class ET(Inverter):
|
|
|
252
256
|
Apparent4("meter_apparent_power_total", 36041, "Meter Apparent Power Total", Kind.GRID),
|
|
253
257
|
Integer("meter_type", 36043, "Meter Type", "", Kind.GRID), # (0: Single phase, 1: 3P3W, 2: 3P4W, 3: HomeKit)
|
|
254
258
|
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
|
|
259
|
+
# Sensors added in some ARM fw update, read when flag _has_meter_extended is on
|
|
257
260
|
Power4S("meter2_active_power", 36045, "Meter 2 Active Power", Kind.GRID),
|
|
258
261
|
Float("meter2_e_total_exp", 36047, 1000, "Meter 2 Total Energy (export)", "kWh", Kind.GRID),
|
|
259
262
|
Float("meter2_e_total_imp", 36049, 1000, "Meter 2 Total Energy (import)", "kWh", Kind.GRID),
|
|
@@ -264,15 +267,6 @@ class ET(Inverter):
|
|
|
264
267
|
Current("meter_current1", 36055, "Meter L1 Current", Kind.GRID),
|
|
265
268
|
Current("meter_current2", 36056, "Meter L2 Current", Kind.GRID),
|
|
266
269
|
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
270
|
)
|
|
277
271
|
|
|
278
272
|
# Inverter's MPPT data
|
|
@@ -415,25 +409,23 @@ class ET(Inverter):
|
|
|
415
409
|
Integer("eco_mode_enable", 47612, "Eco Mode Switch"),
|
|
416
410
|
)
|
|
417
411
|
|
|
418
|
-
def __init__(self, host: str, comm_addr: int = 0, timeout: int = 1, retries: int = 3):
|
|
419
|
-
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)
|
|
420
414
|
if not self.comm_addr:
|
|
421
415
|
# Set the default inverter address
|
|
422
416
|
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)
|
|
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)
|
|
431
424
|
self._has_eco_mode_v2: bool = True
|
|
432
425
|
self._has_peak_shaving: bool = True
|
|
433
426
|
self._has_battery: bool = True
|
|
434
427
|
self._has_battery2: bool = False
|
|
435
428
|
self._has_meter_extended: bool = False
|
|
436
|
-
self._has_meter_extended2: bool = False
|
|
437
429
|
self._has_mppt: bool = False
|
|
438
430
|
self._sensors = self.__all_sensors
|
|
439
431
|
self._sensors_battery = self.__all_sensors_battery
|
|
@@ -452,11 +444,6 @@ class ET(Inverter):
|
|
|
452
444
|
"""Filter to exclude extended meter sensors"""
|
|
453
445
|
return s.offset < 36045
|
|
454
446
|
|
|
455
|
-
@staticmethod
|
|
456
|
-
def _not_extended_meter2(s: Sensor) -> bool:
|
|
457
|
-
"""Filter to exclude extended meter sensors"""
|
|
458
|
-
return s.offset < 36058
|
|
459
|
-
|
|
460
447
|
async def read_device_info(self):
|
|
461
448
|
response = await self._read_from_socket(self._READ_DEVICE_VERSION_INFO)
|
|
462
449
|
response = response.response_data()
|
|
@@ -487,36 +474,29 @@ class ET(Inverter):
|
|
|
487
474
|
if is_2_battery(self) or self.rated_power >= 25000:
|
|
488
475
|
self._has_battery2 = True
|
|
489
476
|
|
|
490
|
-
if
|
|
477
|
+
if self.rated_power >= 15000:
|
|
491
478
|
self._has_mppt = True
|
|
492
479
|
self._has_meter_extended = True
|
|
493
|
-
self._has_meter_extended2 = True
|
|
494
480
|
else:
|
|
495
481
|
self._sensors_meter = tuple(filter(self._not_extended_meter, self._sensors_meter))
|
|
496
482
|
|
|
497
483
|
# Check and add EcoModeV2 settings added in (ETU fw 19)
|
|
498
484
|
try:
|
|
499
|
-
await self._read_from_socket(
|
|
485
|
+
await self._read_from_socket(self._read_command(47547, 6))
|
|
500
486
|
self._settings.update({s.id_: s for s in self.__settings_arm_fw_19})
|
|
501
487
|
except RequestRejectedException as ex:
|
|
502
488
|
if ex.message == 'ILLEGAL DATA ADDRESS':
|
|
503
|
-
logger.debug("EcoModeV2 settings
|
|
489
|
+
logger.debug("Cannot read EcoModeV2 settings, using to EcoModeV1.")
|
|
504
490
|
self._has_eco_mode_v2 = False
|
|
505
|
-
except RequestFailedException as ex:
|
|
506
|
-
logger.debug("Cannot read EcoModeV2 settings, switching to EcoModeV1.")
|
|
507
|
-
self._has_eco_mode_v2 = False
|
|
508
491
|
|
|
509
492
|
# Check and add Peak Shaving settings added in (ETU fw 22)
|
|
510
493
|
try:
|
|
511
|
-
await self._read_from_socket(
|
|
494
|
+
await self._read_from_socket(self._read_command(47589, 6))
|
|
512
495
|
self._settings.update({s.id_: s for s in self.__settings_arm_fw_22})
|
|
513
496
|
except RequestRejectedException as ex:
|
|
514
497
|
if ex.message == 'ILLEGAL DATA ADDRESS':
|
|
515
|
-
logger.debug("PeakShaving setting
|
|
498
|
+
logger.debug("Cannot read PeakShaving setting, disabling it.")
|
|
516
499
|
self._has_peak_shaving = False
|
|
517
|
-
except RequestFailedException as ex:
|
|
518
|
-
logger.debug("Cannot read _has_peak_shaving settings, disabling it.")
|
|
519
|
-
self._has_peak_shaving = False
|
|
520
500
|
|
|
521
501
|
async def read_runtime_data(self) -> Dict[str, Any]:
|
|
522
502
|
response = await self._read_from_socket(self._READ_RUNNING_DATA)
|
|
@@ -529,7 +509,7 @@ class ET(Inverter):
|
|
|
529
509
|
data.update(self._map_response(response, self._sensors_battery))
|
|
530
510
|
except RequestRejectedException as ex:
|
|
531
511
|
if ex.message == 'ILLEGAL DATA ADDRESS':
|
|
532
|
-
logger.warning("
|
|
512
|
+
logger.warning("Cannot read battery values, disabling further attempts.")
|
|
533
513
|
self._has_battery = False
|
|
534
514
|
else:
|
|
535
515
|
raise ex
|
|
@@ -540,32 +520,18 @@ class ET(Inverter):
|
|
|
540
520
|
self._map_response(response, self._sensors_battery2))
|
|
541
521
|
except RequestRejectedException as ex:
|
|
542
522
|
if ex.message == 'ILLEGAL DATA ADDRESS':
|
|
543
|
-
logger.warning("
|
|
523
|
+
logger.warning("Cannot read battery 2 values, disabling further attempts.")
|
|
544
524
|
self._has_battery2 = False
|
|
545
525
|
else:
|
|
546
526
|
raise ex
|
|
547
527
|
|
|
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:
|
|
528
|
+
if self._has_meter_extended:
|
|
563
529
|
try:
|
|
564
530
|
response = await self._read_from_socket(self._READ_METER_DATA_EXTENDED)
|
|
565
531
|
data.update(self._map_response(response, self._sensors_meter))
|
|
566
532
|
except RequestRejectedException as ex:
|
|
567
533
|
if ex.message == 'ILLEGAL DATA ADDRESS':
|
|
568
|
-
logger.warning("
|
|
534
|
+
logger.warning("Cannot read extended meter values, disabling further attempts.")
|
|
569
535
|
self._has_meter_extended = False
|
|
570
536
|
self._sensors_meter = tuple(filter(self._not_extended_meter, self._sensors_meter))
|
|
571
537
|
response = await self._read_from_socket(self._READ_METER_DATA)
|
|
@@ -583,7 +549,7 @@ class ET(Inverter):
|
|
|
583
549
|
data.update(self._map_response(response, self._sensors_mppt))
|
|
584
550
|
except RequestRejectedException as ex:
|
|
585
551
|
if ex.message == 'ILLEGAL DATA ADDRESS':
|
|
586
|
-
logger.warning("MPPT values
|
|
552
|
+
logger.warning("Cannot read MPPT values, disabling further attempts.")
|
|
587
553
|
self._has_mppt = False
|
|
588
554
|
else:
|
|
589
555
|
raise ex
|
|
@@ -598,7 +564,7 @@ class ET(Inverter):
|
|
|
598
564
|
|
|
599
565
|
async def _read_setting(self, setting: Sensor) -> Any:
|
|
600
566
|
count = (setting.size_ + (setting.size_ % 2)) // 2
|
|
601
|
-
response = await self._read_from_socket(
|
|
567
|
+
response = await self._read_from_socket(self._read_command(setting.offset, count))
|
|
602
568
|
return setting.read_value(response)
|
|
603
569
|
|
|
604
570
|
async def write_setting(self, setting_id: str, value: Any):
|
|
@@ -610,15 +576,15 @@ class ET(Inverter):
|
|
|
610
576
|
async def _write_setting(self, setting: Sensor, value: Any):
|
|
611
577
|
if setting.size_ == 1:
|
|
612
578
|
# modbus can address/store only 16 bit values, read the other 8 bytes
|
|
613
|
-
response = await self._read_from_socket(
|
|
579
|
+
response = await self._read_from_socket(self._read_command(setting.offset, 1))
|
|
614
580
|
raw_value = setting.encode_value(value, response.response_data()[0:2])
|
|
615
581
|
else:
|
|
616
582
|
raw_value = setting.encode_value(value)
|
|
617
583
|
if len(raw_value) <= 2:
|
|
618
584
|
value = int.from_bytes(raw_value, byteorder="big", signed=True)
|
|
619
|
-
await self._read_from_socket(
|
|
585
|
+
await self._read_from_socket(self._write_command(setting.offset, value))
|
|
620
586
|
else:
|
|
621
|
-
await self._read_from_socket(
|
|
587
|
+
await self._read_from_socket(self._write_multi_command(setting.offset, raw_value))
|
|
622
588
|
|
|
623
589
|
async def read_settings_data(self) -> Dict[str, Any]:
|
|
624
590
|
data = {}
|
|
@@ -649,7 +615,7 @@ class ET(Inverter):
|
|
|
649
615
|
result.remove(OperationMode.ECO_DISCHARGE)
|
|
650
616
|
return tuple(result)
|
|
651
617
|
|
|
652
|
-
async def get_operation_mode(self) -> OperationMode:
|
|
618
|
+
async def get_operation_mode(self) -> OperationMode | None:
|
|
653
619
|
mode_id = await self.read_setting('work_mode')
|
|
654
620
|
try:
|
|
655
621
|
mode = OperationMode(mode_id)
|
|
@@ -737,8 +703,8 @@ class ET(Inverter):
|
|
|
737
703
|
return tuple(self._settings.values())
|
|
738
704
|
|
|
739
705
|
async def _clear_battery_mode_param(self) -> None:
|
|
740
|
-
await self._read_from_socket(
|
|
706
|
+
await self._read_from_socket(self._write_command(0xb9ad, 1))
|
|
741
707
|
|
|
742
708
|
async def _set_offline(self, mode: bool) -> None:
|
|
743
709
|
value = bytes.fromhex('00070000') if mode else bytes.fromhex('00010000')
|
|
744
|
-
await self._read_from_socket(
|
|
710
|
+
await self._read_from_socket(self._write_multi_command(0xb997, value))
|
goodwe/inverter.py
CHANGED
|
@@ -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"""
|