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/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.host: str = host
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 _ensure_lock(self) -> asyncio.Lock:
113
- """Validate (or create) asyncio Lock.
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
- The asyncio.Lock must always be created from within's asyncio loop,
116
- so it cannot be eagerly created in constructor.
117
- Additionally, since asyncio.run() creates and closes its own loop,
118
- the lock's scope (its creating loop) mus be verified to support proper
119
- behavior in subsequent asyncio.run() invocations.
120
- """
121
- if self._lock and self._running_loop == asyncio.get_event_loop():
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
- async with self._ensure_lock():
131
- try:
132
- result = await command.execute(self.host, self.timeout, self.retries)
133
- self._consecutive_failures_count = 0
134
- return result
135
- except MaxRetriesException:
136
- self._consecutive_failures_count += 1
137
- raise RequestFailedException(f'No valid response received even after {self.retries} retries',
138
- self._consecutive_failures_count)
139
- except RequestFailedException as ex:
140
- self._consecutive_failures_count += 1
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 udp command (as bytes).
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: "ILLEGAL DATA ADDRESS",
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 create_modbus_request(comm_addr: int, cmd: int, offset: int, value: int) -> bytes:
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 create_modbus_multi_request(comm_addr: int, cmd: int, offset: int, values: bytes) -> bytes:
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
- Create modbus (multi value) request.
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 validate_modbus_response(data: bytes, cmd: int, offset: int, value: int) -> bool:
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)