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/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
|
|
|
@@ -23,6 +22,7 @@ class SensorKind(Enum):
|
|
|
23
22
|
UPS - inverter ups/eps/backup output (e.g. ac voltage of backup/off-grid connected output)
|
|
24
23
|
BAT - battery (e.g. dc voltage of connected battery pack)
|
|
25
24
|
GRID - power grid/smart meter (e.g. active power exported to grid)
|
|
25
|
+
BMS - BMS direct data (e.g. dc voltage of)
|
|
26
26
|
"""
|
|
27
27
|
|
|
28
28
|
PV = 1
|
|
@@ -30,6 +30,7 @@ class SensorKind(Enum):
|
|
|
30
30
|
UPS = 3
|
|
31
31
|
BAT = 4
|
|
32
32
|
GRID = 5
|
|
33
|
+
BMS = 6
|
|
33
34
|
|
|
34
35
|
|
|
35
36
|
@dataclass
|
|
@@ -87,15 +88,12 @@ class Inverter(ABC):
|
|
|
87
88
|
Represents the inverter state and its basic behavior
|
|
88
89
|
"""
|
|
89
90
|
|
|
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
|
|
91
|
+
def __init__(self, host: str, port: int, comm_addr: int = 0, timeout: int = 1, retries: int = 3):
|
|
92
|
+
self._protocol: InverterProtocol = self._create_protocol(host, port, timeout, retries)
|
|
97
93
|
self._consecutive_failures_count: int = 0
|
|
98
94
|
|
|
95
|
+
self.comm_addr: int = comm_addr
|
|
96
|
+
|
|
99
97
|
self.model_name: str | None = None
|
|
100
98
|
self.serial_number: str | None = None
|
|
101
99
|
self.rated_power: int = 0
|
|
@@ -109,36 +107,30 @@ class Inverter(ABC):
|
|
|
109
107
|
self.arm_version: int = 0
|
|
110
108
|
self.arm_svn_version: int | None = None
|
|
111
109
|
|
|
112
|
-
def
|
|
113
|
-
"""
|
|
110
|
+
def _read_command(self, offset: int, count: int) -> ProtocolCommand:
|
|
111
|
+
"""Create read protocol command."""
|
|
112
|
+
return self._protocol.read_command(self.comm_addr, offset, count)
|
|
114
113
|
|
|
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
|
|
114
|
+
def _write_command(self, register: int, value: int) -> ProtocolCommand:
|
|
115
|
+
"""Create write protocol command."""
|
|
116
|
+
return self._protocol.write_command(self.comm_addr, register, value)
|
|
117
|
+
|
|
118
|
+
def _write_multi_command(self, offset: int, values: bytes) -> ProtocolCommand:
|
|
119
|
+
"""Create write multiple protocol command."""
|
|
120
|
+
return self._protocol.write_multi_command(self.comm_addr, offset, values)
|
|
128
121
|
|
|
129
122
|
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)
|
|
123
|
+
try:
|
|
124
|
+
result = await command.execute(self._protocol)
|
|
125
|
+
self._consecutive_failures_count = 0
|
|
126
|
+
return result
|
|
127
|
+
except MaxRetriesException:
|
|
128
|
+
self._consecutive_failures_count += 1
|
|
129
|
+
raise RequestFailedException(f'No valid response received even after {self._protocol.retries} retries',
|
|
130
|
+
self._consecutive_failures_count) from None
|
|
131
|
+
except RequestFailedException as ex:
|
|
132
|
+
self._consecutive_failures_count += 1
|
|
133
|
+
raise RequestFailedException(ex.message, self._consecutive_failures_count) from None
|
|
142
134
|
|
|
143
135
|
@abstractmethod
|
|
144
136
|
async def read_device_info(self):
|
|
@@ -190,8 +182,8 @@ class Inverter(ABC):
|
|
|
190
182
|
self, command: bytes, validator: Callable[[bytes], bool] = lambda x: True
|
|
191
183
|
) -> ProtocolResponse:
|
|
192
184
|
"""
|
|
193
|
-
Send low level
|
|
194
|
-
Answer command's raw response data.
|
|
185
|
+
Send low level command (as bytes).
|
|
186
|
+
Answer ProtocolResponse with command's raw response data.
|
|
195
187
|
"""
|
|
196
188
|
return await self._read_from_socket(ProtocolCommand(command, validator))
|
|
197
189
|
|
|
@@ -277,6 +269,13 @@ class Inverter(ABC):
|
|
|
277
269
|
"""
|
|
278
270
|
raise NotImplementedError()
|
|
279
271
|
|
|
272
|
+
@staticmethod
|
|
273
|
+
def _create_protocol(host: str, port: int, timeout: int, retries: int) -> InverterProtocol:
|
|
274
|
+
if port == 502:
|
|
275
|
+
return TcpInverterProtocol(host, port, timeout, retries)
|
|
276
|
+
else:
|
|
277
|
+
return UdpInverterProtocol(host, port, timeout, retries)
|
|
278
|
+
|
|
280
279
|
@staticmethod
|
|
281
280
|
def _map_response(response: ProtocolResponse, sensors: Tuple[Sensor, ...]) -> Dict[str, Any]:
|
|
282
281
|
"""Process the response data and return dictionary with runtime values"""
|
goodwe/modbus.py
CHANGED
|
@@ -9,9 +9,11 @@ MODBUS_READ_CMD: int = 0x3
|
|
|
9
9
|
MODBUS_WRITE_CMD: int = 0x6
|
|
10
10
|
MODBUS_WRITE_MULTI_CMD: int = 0x10
|
|
11
11
|
|
|
12
|
+
ILLEGAL_DATA_ADDRESS = 'ILLEGAL DATA ADDRESS'
|
|
13
|
+
|
|
12
14
|
FAILURE_CODES = {
|
|
13
15
|
1: "ILLEGAL FUNCTION",
|
|
14
|
-
2:
|
|
16
|
+
2: ILLEGAL_DATA_ADDRESS,
|
|
15
17
|
3: "ILLEGAL DATA VALUE",
|
|
16
18
|
4: "SLAVE DEVICE FAILURE",
|
|
17
19
|
5: "ACKNOWLEDGE",
|
|
@@ -52,9 +54,9 @@ def _modbus_checksum(data: Union[bytearray, bytes]) -> int:
|
|
|
52
54
|
return crc
|
|
53
55
|
|
|
54
56
|
|
|
55
|
-
def
|
|
57
|
+
def create_modbus_rtu_request(comm_addr: int, cmd: int, offset: int, value: int) -> bytes:
|
|
56
58
|
"""
|
|
57
|
-
Create modbus request.
|
|
59
|
+
Create modbus RTU request.
|
|
58
60
|
data[0] is inverter address
|
|
59
61
|
data[1] is modbus command
|
|
60
62
|
data[2:3] is command offset parameter
|
|
@@ -74,9 +76,36 @@ def create_modbus_request(comm_addr: int, cmd: int, offset: int, value: int) ->
|
|
|
74
76
|
return bytes(data)
|
|
75
77
|
|
|
76
78
|
|
|
77
|
-
def
|
|
79
|
+
def create_modbus_tcp_request(comm_addr: int, cmd: int, offset: int, value: int) -> bytes:
|
|
80
|
+
"""
|
|
81
|
+
Create modbus TCP request.
|
|
82
|
+
data[0:1] is transaction identifier
|
|
83
|
+
data[2:3] is protocol identifier (0)
|
|
84
|
+
data[4:5] message length
|
|
85
|
+
data[6] is inverter address
|
|
86
|
+
data[7] is modbus command
|
|
87
|
+
data[8:9] is command offset parameter
|
|
88
|
+
data[10:11] is command value parameter
|
|
78
89
|
"""
|
|
79
|
-
|
|
90
|
+
data: bytearray = bytearray(12)
|
|
91
|
+
data[0] = 0
|
|
92
|
+
data[1] = 1 # Not transaction ID support yet
|
|
93
|
+
data[2] = 0
|
|
94
|
+
data[3] = 0
|
|
95
|
+
data[4] = 0
|
|
96
|
+
data[5] = 6
|
|
97
|
+
data[6] = comm_addr
|
|
98
|
+
data[7] = cmd
|
|
99
|
+
data[8] = (offset >> 8) & 0xFF
|
|
100
|
+
data[9] = offset & 0xFF
|
|
101
|
+
data[10] = (value >> 8) & 0xFF
|
|
102
|
+
data[11] = value & 0xFF
|
|
103
|
+
return bytes(data)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def create_modbus_rtu_multi_request(comm_addr: int, cmd: int, offset: int, values: bytes) -> bytes:
|
|
107
|
+
"""
|
|
108
|
+
Create modbus RTU (multi value) request.
|
|
80
109
|
data[0] is inverter address
|
|
81
110
|
data[1] is modbus command
|
|
82
111
|
data[2:3] is command offset parameter
|
|
@@ -100,9 +129,40 @@ def create_modbus_multi_request(comm_addr: int, cmd: int, offset: int, values: b
|
|
|
100
129
|
return bytes(data)
|
|
101
130
|
|
|
102
131
|
|
|
103
|
-
def
|
|
132
|
+
def create_modbus_tcp_multi_request(comm_addr: int, cmd: int, offset: int, values: bytes) -> bytes:
|
|
133
|
+
"""
|
|
134
|
+
Create modbus TCP (multi value) request.
|
|
135
|
+
data[0:1] is transaction identifier
|
|
136
|
+
data[2:3] is protocol identifier (0)
|
|
137
|
+
data[4:5] message length
|
|
138
|
+
data[6] is inverter address
|
|
139
|
+
data[7] is modbus command
|
|
140
|
+
data[8:9] is command offset parameter
|
|
141
|
+
data[10:11] is number of registers
|
|
142
|
+
data[12] is number of bytes
|
|
143
|
+
data[13-n] is data payload
|
|
144
|
+
"""
|
|
145
|
+
data: bytearray = bytearray(13)
|
|
146
|
+
data[0] = 0
|
|
147
|
+
data[1] = 1 # Not transaction ID support yet
|
|
148
|
+
data[2] = 0
|
|
149
|
+
data[3] = 0
|
|
150
|
+
data[4] = 0
|
|
151
|
+
data[5] = 7 + len(values)
|
|
152
|
+
data[6] = comm_addr
|
|
153
|
+
data[7] = cmd
|
|
154
|
+
data[8] = (offset >> 8) & 0xFF
|
|
155
|
+
data[9] = offset & 0xFF
|
|
156
|
+
data[10] = 0
|
|
157
|
+
data[11] = len(values) // 2
|
|
158
|
+
data[12] = len(values)
|
|
159
|
+
data.extend(values)
|
|
160
|
+
return bytes(data)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def validate_modbus_rtu_response(data: bytes, cmd: int, offset: int, value: int) -> bool:
|
|
104
164
|
"""
|
|
105
|
-
Validate the modbus response.
|
|
165
|
+
Validate the modbus RTU response.
|
|
106
166
|
data[0:1] is header
|
|
107
167
|
data[2] is source address
|
|
108
168
|
data[3] is command return type
|
|
@@ -147,3 +207,45 @@ def validate_modbus_response(data: bytes, cmd: int, offset: int, value: int) ->
|
|
|
147
207
|
raise RequestRejectedException(failure_code)
|
|
148
208
|
|
|
149
209
|
return True
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def validate_modbus_tcp_response(data: bytes, cmd: int, offset: int, value: int) -> bool:
|
|
213
|
+
"""
|
|
214
|
+
Validate the modbus TCP response.
|
|
215
|
+
data[0:1] is transaction identifier
|
|
216
|
+
data[2:3] is protocol identifier (0)
|
|
217
|
+
data[4:5] message length
|
|
218
|
+
data[6] is source address
|
|
219
|
+
data[7] is command return type
|
|
220
|
+
data[8] is response payload length (for read commands)
|
|
221
|
+
"""
|
|
222
|
+
if len(data) <= 8:
|
|
223
|
+
logger.debug("Response is too short.")
|
|
224
|
+
return False
|
|
225
|
+
if data[7] == MODBUS_READ_CMD:
|
|
226
|
+
if data[8] != value * 2:
|
|
227
|
+
logger.debug("Response has unexpected length: %d, expected %d.", data[8], value * 2)
|
|
228
|
+
return False
|
|
229
|
+
expected_length = data[8] + 9
|
|
230
|
+
if len(data) < expected_length:
|
|
231
|
+
logger.debug("Response is too short: %d, expected %d.", len(data), expected_length)
|
|
232
|
+
return False
|
|
233
|
+
elif data[7] in (MODBUS_WRITE_CMD, MODBUS_WRITE_MULTI_CMD):
|
|
234
|
+
if len(data) < 12:
|
|
235
|
+
logger.debug("Response has unexpected length: %d, expected %d.", len(data), 14)
|
|
236
|
+
return False
|
|
237
|
+
response_offset = int.from_bytes(data[8:10], byteorder='big', signed=False)
|
|
238
|
+
if response_offset != offset:
|
|
239
|
+
logger.debug("Response has wrong offset: %X, expected %X.", response_offset, offset)
|
|
240
|
+
return False
|
|
241
|
+
response_value = int.from_bytes(data[10:12], byteorder='big', signed=True)
|
|
242
|
+
if response_value != value:
|
|
243
|
+
logger.debug("Response has wrong value: %X, expected %X.", response_value, value)
|
|
244
|
+
return False
|
|
245
|
+
|
|
246
|
+
if data[7] != cmd:
|
|
247
|
+
failure_code = FAILURE_CODES.get(data[8], "UNKNOWN")
|
|
248
|
+
logger.debug("Response is command failure: %s.", FAILURE_CODES.get(data[8], "UNKNOWN"))
|
|
249
|
+
raise RequestRejectedException(failure_code)
|
|
250
|
+
|
|
251
|
+
return True
|
goodwe/model.py
CHANGED
|
@@ -48,7 +48,3 @@ def is_2_battery(inverter: Inverter) -> bool:
|
|
|
48
48
|
def is_745_platform(inverter: Inverter) -> bool:
|
|
49
49
|
return any(model in inverter.serial_number for model in PLATFORM_745_LV_MODELS) or any(
|
|
50
50
|
model in inverter.serial_number for model in PLATFORM_745_HV_MODELS)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def is_753_platform(inverter: Inverter) -> bool:
|
|
54
|
-
return any(model in inverter.serial_number for model in PLATFORM_753_MODELS)
|