goodwe 0.4.1__py3-none-any.whl → 0.4.2__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/dt.py CHANGED
@@ -1,15 +1,19 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import logging
3
4
  from typing import Tuple
4
5
 
5
- from .exceptions import InverterError
6
+ from .exceptions import InverterError, RequestRejectedException
6
7
  from .inverter import Inverter
7
8
  from .inverter import OperationMode
8
9
  from .inverter import SensorKind as Kind
10
+ from .modbus import ILLEGAL_DATA_ADDRESS
9
11
  from .model import is_3_mppt, is_single_phase
10
12
  from .protocol import ProtocolCommand
11
13
  from .sensor import *
12
14
 
15
+ logger = logging.getLogger(__name__)
16
+
13
17
 
14
18
  class DT(Inverter):
15
19
  """Class representing inverter of DT/MS/D-NS/XS or GE's GEP(PSB/PSC) families"""
@@ -123,10 +127,7 @@ class DT(Inverter):
123
127
  )
124
128
 
125
129
  def __init__(self, host: str, port: int, comm_addr: int = 0, timeout: int = 1, retries: int = 3):
126
- super().__init__(host, port, comm_addr, timeout, retries)
127
- if not self.comm_addr:
128
- # Set the default inverter address
129
- self.comm_addr = 0x7f
130
+ super().__init__(host, port, comm_addr if comm_addr else 0x7f, timeout, retries)
130
131
  self._READ_DEVICE_VERSION_INFO: ProtocolCommand = self._read_command(0x7531, 0x0028)
131
132
  self._READ_DEVICE_RUNNING_DATA: ProtocolCommand = self._read_command(0x7594, 0x0049)
132
133
  self._sensors = self.__all_sensors
@@ -177,17 +178,43 @@ class DT(Inverter):
177
178
 
178
179
  async def read_setting(self, setting_id: str) -> Any:
179
180
  setting = self._settings.get(setting_id)
180
- if not setting:
181
- raise ValueError(f'Unknown setting "{setting_id}"')
182
- count = (setting.size_ + (setting.size_ % 2)) // 2
183
- response = await self._read_from_socket(self._read_command(setting.offset, count))
184
- return setting.read_value(response)
181
+ if setting:
182
+ return await self._read_setting(setting)
183
+ else:
184
+ if setting_id.startswith("modbus"):
185
+ response = await self._read_from_socket(self._read_command(int(setting_id[7:]), 1))
186
+ return int.from_bytes(response.read(2), byteorder="big", signed=True)
187
+ else:
188
+ raise ValueError(f'Unknown setting "{setting_id}"')
189
+
190
+ async def _read_setting(self, setting: Sensor) -> Any:
191
+ try:
192
+ count = (setting.size_ + (setting.size_ % 2)) // 2
193
+ response = await self._read_from_socket(self._read_command(setting.offset, count))
194
+ return setting.read_value(response)
195
+ except RequestRejectedException as ex:
196
+ if ex.message == ILLEGAL_DATA_ADDRESS:
197
+ logger.debug("Unsupported setting %s", setting.id_)
198
+ self._settings.pop(setting.id_, None)
199
+ return None
185
200
 
186
201
  async def write_setting(self, setting_id: str, value: Any):
187
202
  setting = self._settings.get(setting_id)
188
- if not setting:
189
- raise ValueError(f'Unknown setting "{setting_id}"')
190
- raw_value = setting.encode_value(value)
203
+ if setting:
204
+ await self._write_setting(setting, value)
205
+ else:
206
+ if setting_id.startswith("modbus"):
207
+ await self._read_from_socket(self._write_command(int(setting_id[7:]), int(value)))
208
+ else:
209
+ raise ValueError(f'Unknown setting "{setting_id}"')
210
+
211
+ async def _write_setting(self, setting: Sensor, value: Any):
212
+ if setting.size_ == 1:
213
+ # modbus can address/store only 16 bit values, read the other 8 bytes
214
+ response = await self._read_from_socket(self._read_command(setting.offset, 1))
215
+ raw_value = setting.encode_value(value, response.response_data()[0:2])
216
+ else:
217
+ raw_value = setting.encode_value(value)
191
218
  if len(raw_value) <= 2:
192
219
  value = int.from_bytes(raw_value, byteorder="big", signed=True)
193
220
  await self._read_from_socket(self._write_command(setting.offset, value))
goodwe/es.py CHANGED
@@ -168,10 +168,7 @@ class ES(Inverter):
168
168
  )
169
169
 
170
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)
172
- if not self.comm_addr:
173
- # Set the default inverter address
174
- self.comm_addr = 0xf7
171
+ super().__init__(host, port, comm_addr if comm_addr else 0xf7, timeout, retries)
175
172
  self._settings: dict[str, Sensor] = {s.id_: s for s in self.__all_settings}
176
173
 
177
174
  def _supports_eco_mode_v2(self) -> bool:
@@ -220,6 +217,9 @@ class ES(Inverter):
220
217
  if not setting:
221
218
  raise ValueError(f'Unknown setting "{setting_id}"')
222
219
  return await self._read_setting(setting)
220
+ elif setting_id.startswith("modbus"):
221
+ response = await self._read_from_socket(self._read_command(int(setting_id[7:]), 1))
222
+ return int.from_bytes(response.read(2), byteorder="big", signed=True)
223
223
  else:
224
224
  all_settings = await self.read_settings_data()
225
225
  return all_settings.get(setting_id)
@@ -238,6 +238,8 @@ class ES(Inverter):
238
238
  await self._read_from_socket(
239
239
  Aa55ProtocolCommand("030206" + Timestamp("time", 0, "").encode_value(value).hex(), "0382")
240
240
  )
241
+ elif setting_id.startswith("modbus"):
242
+ await self._read_from_socket(self._write_command(int(setting_id[7:]), int(value)))
241
243
  else:
242
244
  setting: Sensor | None = self._settings.get(setting_id)
243
245
  if not setting:
@@ -249,10 +251,9 @@ class ES(Inverter):
249
251
  # modbus can address/store only 16 bit values, read the other 8 bytes
250
252
  if self._is_modbus_setting(setting):
251
253
  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
254
  else:
254
255
  response = await self._read_from_socket(Aa55ReadCommand(setting.offset, 1))
255
- raw_value = setting.encode_value(value, response.response_data()[2:4])
256
+ raw_value = setting.encode_value(value, response.response_data()[0:2])
256
257
  else:
257
258
  raw_value = setting.encode_value(value)
258
259
  if len(raw_value) <= 2:
goodwe/et.py CHANGED
@@ -457,10 +457,7 @@ class ET(Inverter):
457
457
  )
458
458
 
459
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)
461
- if not self.comm_addr:
462
- # Set the default inverter address
463
- self.comm_addr = 0xf7
460
+ super().__init__(host, port, comm_addr if comm_addr else 0xf7, timeout, retries)
464
461
  self._READ_DEVICE_VERSION_INFO: ProtocolCommand = self._read_command(0x88b8, 0x0021)
465
462
  self._READ_RUNNING_DATA: ProtocolCommand = self._read_command(0x891c, 0x007d)
466
463
  self._READ_METER_DATA: ProtocolCommand = self._read_command(0x8ca0, 0x2d)
@@ -611,26 +608,35 @@ class ET(Inverter):
611
608
 
612
609
  async def read_setting(self, setting_id: str) -> Any:
613
610
  setting = self._settings.get(setting_id)
614
- if not setting:
615
- raise ValueError(f'Unknown setting "{setting_id}"')
616
- try:
611
+ if setting:
617
612
  return await self._read_setting(setting)
613
+ else:
614
+ if setting_id.startswith("modbus"):
615
+ response = await self._read_from_socket(self._read_command(int(setting_id[7:]), 1))
616
+ return int.from_bytes(response.read(2), byteorder="big", signed=True)
617
+ else:
618
+ raise ValueError(f'Unknown setting "{setting_id}"')
619
+
620
+ async def _read_setting(self, setting: Sensor) -> Any:
621
+ try:
622
+ count = (setting.size_ + (setting.size_ % 2)) // 2
623
+ response = await self._read_from_socket(self._read_command(setting.offset, count))
624
+ return setting.read_value(response)
618
625
  except RequestRejectedException as ex:
619
626
  if ex.message == ILLEGAL_DATA_ADDRESS:
620
627
  logger.debug("Unsupported setting %s", setting.id_)
621
- self._settings.pop(setting_id, None)
628
+ self._settings.pop(setting.id_, None)
622
629
  return None
623
630
 
624
- async def _read_setting(self, setting: Sensor) -> Any:
625
- count = (setting.size_ + (setting.size_ % 2)) // 2
626
- response = await self._read_from_socket(self._read_command(setting.offset, count))
627
- return setting.read_value(response)
628
-
629
631
  async def write_setting(self, setting_id: str, value: Any):
630
632
  setting = self._settings.get(setting_id)
631
- if not setting:
632
- raise ValueError(f'Unknown setting "{setting_id}"')
633
- await self._write_setting(setting, value)
633
+ if setting:
634
+ await self._write_setting(setting, value)
635
+ else:
636
+ if setting_id.startswith("modbus"):
637
+ await self._read_from_socket(self._write_command(int(setting_id[7:]), int(value)))
638
+ else:
639
+ raise ValueError(f'Unknown setting "{setting_id}"')
634
640
 
635
641
  async def _write_setting(self, setting: Sensor, value: Any):
636
642
  if setting.size_ == 1:
goodwe/exceptions.py CHANGED
@@ -29,5 +29,19 @@ class RequestRejectedException(InverterError):
29
29
  self.message: str = message
30
30
 
31
31
 
32
+ class PartialResponseException(InverterError):
33
+ """
34
+ Indicates the received response data are incomplete and is probably fragmented to multiple packets.
35
+
36
+ Attributes:
37
+ length -- received data length
38
+ expected -- expected data lenght
39
+ """
40
+
41
+ def __init__(self, lenght: int, expected: int):
42
+ self.length: int = lenght
43
+ self.expected: int = expected
44
+
45
+
32
46
  class MaxRetriesException(InverterError):
33
47
  """Indicates the maximum number of retries has been reached"""
goodwe/inverter.py CHANGED
@@ -89,10 +89,9 @@ class Inverter(ABC):
89
89
  """
90
90
 
91
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)
92
+ self._protocol: InverterProtocol = self._create_protocol(host, port, comm_addr, timeout, retries)
93
93
  self._consecutive_failures_count: int = 0
94
-
95
- self.comm_addr: int = comm_addr
94
+ self.keep_alive: bool = True
96
95
 
97
96
  self.model_name: str | None = None
98
97
  self.serial_number: str | None = None
@@ -109,15 +108,15 @@ class Inverter(ABC):
109
108
 
110
109
  def _read_command(self, offset: int, count: int) -> ProtocolCommand:
111
110
  """Create read protocol command."""
112
- return self._protocol.read_command(self.comm_addr, offset, count)
111
+ return self._protocol.read_command(offset, count)
113
112
 
114
113
  def _write_command(self, register: int, value: int) -> ProtocolCommand:
115
114
  """Create write protocol command."""
116
- return self._protocol.write_command(self.comm_addr, register, value)
115
+ return self._protocol.write_command(register, value)
117
116
 
118
117
  def _write_multi_command(self, offset: int, values: bytes) -> ProtocolCommand:
119
118
  """Create write multiple protocol command."""
120
- return self._protocol.write_multi_command(self.comm_addr, offset, values)
119
+ return self._protocol.write_multi_command(offset, values)
121
120
 
122
121
  async def _read_from_socket(self, command: ProtocolCommand) -> ProtocolResponse:
123
122
  try:
@@ -131,6 +130,9 @@ class Inverter(ABC):
131
130
  except RequestFailedException as ex:
132
131
  self._consecutive_failures_count += 1
133
132
  raise RequestFailedException(ex.message, self._consecutive_failures_count) from None
133
+ finally:
134
+ if not self.keep_alive:
135
+ self._protocol.close_transport()
134
136
 
135
137
  @abstractmethod
136
138
  async def read_device_info(self):
@@ -270,11 +272,11 @@ class Inverter(ABC):
270
272
  raise NotImplementedError()
271
273
 
272
274
  @staticmethod
273
- def _create_protocol(host: str, port: int, timeout: int, retries: int) -> InverterProtocol:
275
+ def _create_protocol(host: str, port: int, comm_addr: int, timeout: int, retries: int) -> InverterProtocol:
274
276
  if port == 502:
275
- return TcpInverterProtocol(host, port, timeout, retries)
277
+ return TcpInverterProtocol(host, port, comm_addr, timeout, retries)
276
278
  else:
277
- return UdpInverterProtocol(host, port, timeout, retries)
279
+ return UdpInverterProtocol(host, port, comm_addr, timeout, retries)
278
280
 
279
281
  @staticmethod
280
282
  def _map_response(response: ProtocolResponse, sensors: Tuple[Sensor, ...]) -> Dict[str, Any]:
goodwe/modbus.py CHANGED
@@ -1,7 +1,7 @@
1
1
  import logging
2
2
  from typing import Union
3
3
 
4
- from .exceptions import RequestRejectedException
4
+ from .exceptions import PartialResponseException, RequestRejectedException
5
5
 
6
6
  logger = logging.getLogger(__name__)
7
7
 
@@ -9,7 +9,7 @@ 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'
12
+ ILLEGAL_DATA_ADDRESS: str = 'ILLEGAL DATA ADDRESS'
13
13
 
14
14
  FAILURE_CODES = {
15
15
  1: "ILLEGAL FUNCTION",
@@ -178,8 +178,7 @@ def validate_modbus_rtu_response(data: bytes, cmd: int, offset: int, value: int)
178
178
  return False
179
179
  expected_length = data[4] + 7
180
180
  if len(data) < expected_length:
181
- logger.debug("Response is too short: %d, expected %d.", len(data), expected_length)
182
- return False
181
+ raise PartialResponseException(len(data), expected_length)
183
182
  elif data[3] in (MODBUS_WRITE_CMD, MODBUS_WRITE_MULTI_CMD):
184
183
  if len(data) < 10:
185
184
  logger.debug("Response has unexpected length: %d, expected %d.", len(data), 10)
@@ -222,18 +221,18 @@ def validate_modbus_tcp_response(data: bytes, cmd: int, offset: int, value: int)
222
221
  if len(data) <= 8:
223
222
  logger.debug("Response is too short.")
224
223
  return False
224
+ expected_length = int.from_bytes(data[4:6], byteorder='big', signed=False) + 6
225
+ if len(data) < expected_length:
226
+ raise PartialResponseException(len(data), expected_length)
227
+
225
228
  if data[7] == MODBUS_READ_CMD:
226
229
  if data[8] != value * 2:
227
230
  logger.debug("Response has unexpected length: %d, expected %d.", data[8], value * 2)
228
231
  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
232
  elif data[7] in (MODBUS_WRITE_CMD, MODBUS_WRITE_MULTI_CMD):
234
233
  if len(data) < 12:
235
234
  logger.debug("Response has unexpected length: %d, expected %d.", len(data), 14)
236
- return False
235
+ raise PartialResponseException(len(data), expected_length)
237
236
  response_offset = int.from_bytes(data[8:10], byteorder='big', signed=False)
238
237
  if response_offset != offset:
239
238
  logger.debug("Response has wrong offset: %X, expected %X.", response_offset, offset)
goodwe/protocol.py CHANGED
@@ -3,22 +3,35 @@ from __future__ import annotations
3
3
  import asyncio
4
4
  import io
5
5
  import logging
6
+ import platform
7
+ import socket
6
8
  from asyncio.futures import Future
7
9
  from typing import Tuple, Optional, Callable
8
10
 
9
- from .exceptions import MaxRetriesException, RequestFailedException, RequestRejectedException
11
+ from .exceptions import MaxRetriesException, PartialResponseException, RequestFailedException, RequestRejectedException
10
12
  from .modbus import create_modbus_rtu_request, create_modbus_rtu_multi_request, create_modbus_tcp_request, \
11
13
  create_modbus_tcp_multi_request, validate_modbus_rtu_response, validate_modbus_tcp_response, MODBUS_READ_CMD, \
12
14
  MODBUS_WRITE_CMD, MODBUS_WRITE_MULTI_CMD
13
15
 
14
16
  logger = logging.getLogger(__name__)
15
17
 
18
+ _modbus_tcp_tx = 0
19
+
20
+
21
+ def _next_tx() -> bytes:
22
+ global _modbus_tcp_tx
23
+ _modbus_tcp_tx += 1
24
+ if _modbus_tcp_tx == 0xFFFF:
25
+ _modbus_tcp_tx = 1
26
+ return int.to_bytes(_modbus_tcp_tx, length=2, byteorder="big", signed=False)
27
+
16
28
 
17
29
  class InverterProtocol:
18
30
 
19
- def __init__(self, host: str, port: int, timeout: int, retries: int):
31
+ def __init__(self, host: str, port: int, comm_addr: int, timeout: int, retries: int):
20
32
  self._host: str = host
21
33
  self._port: int = port
34
+ self._comm_addr: int = comm_addr
22
35
  self._running_loop: asyncio.AbstractEventLoop | None = None
23
36
  self._lock: asyncio.Lock | None = None
24
37
  self._timer: asyncio.TimerHandle | None = None
@@ -27,6 +40,7 @@ class InverterProtocol:
27
40
  self.protocol: asyncio.Protocol | None = None
28
41
  self.response_future: Future | None = None
29
42
  self.command: ProtocolCommand | None = None
43
+ self._partial_data: bytes | None = None
30
44
 
31
45
  def _ensure_lock(self) -> asyncio.Lock:
32
46
  """Validate (or create) asyncio Lock.
@@ -43,45 +57,47 @@ class InverterProtocol:
43
57
  logger.debug("Creating lock instance for current event loop.")
44
58
  self._lock = asyncio.Lock()
45
59
  self._running_loop = asyncio.get_event_loop()
46
- self._close_transport()
60
+ self.close_transport()
47
61
  return self._lock
48
62
 
49
- def _close_transport(self) -> None:
63
+ def close_transport(self) -> None:
64
+ """Close the underlying transport/connection."""
50
65
  raise NotImplementedError()
51
66
 
52
67
  async def send_request(self, command: ProtocolCommand) -> Future:
68
+ """Convert command to request and send it to inverter."""
53
69
  raise NotImplementedError()
54
70
 
55
- def read_command(self, comm_addr: int, offset: int, count: int) -> ProtocolCommand:
71
+ def read_command(self, offset: int, count: int) -> ProtocolCommand:
56
72
  """Create read protocol command."""
57
73
  raise NotImplementedError()
58
74
 
59
- def write_command(self, comm_addr: int, register: int, value: int) -> ProtocolCommand:
75
+ def write_command(self, register: int, value: int) -> ProtocolCommand:
60
76
  """Create write protocol command."""
61
77
  raise NotImplementedError()
62
78
 
63
- def write_multi_command(self, comm_addr: int, offset: int, values: bytes) -> ProtocolCommand:
79
+ def write_multi_command(self, offset: int, values: bytes) -> ProtocolCommand:
64
80
  """Create write multiple protocol command."""
65
81
  raise NotImplementedError()
66
82
 
67
83
 
68
84
  class UdpInverterProtocol(InverterProtocol, asyncio.DatagramProtocol):
69
- def __init__(self, host: str, port: int, timeout: int = 1, retries: int = 3):
70
- super().__init__(host, port, timeout, retries)
85
+ def __init__(self, host: str, port: int, comm_addr: int, timeout: int = 1, retries: int = 3):
86
+ super().__init__(host, port, comm_addr, timeout, retries)
71
87
  self._transport: asyncio.transports.DatagramTransport | None = None
72
88
  self._retry: int = 0
73
89
 
74
- def read_command(self, comm_addr: int, offset: int, count: int) -> ProtocolCommand:
90
+ def read_command(self, offset: int, count: int) -> ProtocolCommand:
75
91
  """Create read protocol command."""
76
- return ModbusRtuReadCommand(comm_addr, offset, count)
92
+ return ModbusRtuReadCommand(self._comm_addr, offset, count)
77
93
 
78
- def write_command(self, comm_addr: int, register: int, value: int) -> ProtocolCommand:
94
+ def write_command(self, register: int, value: int) -> ProtocolCommand:
79
95
  """Create write protocol command."""
80
- return ModbusRtuWriteCommand(comm_addr, register, value)
96
+ return ModbusRtuWriteCommand(self._comm_addr, register, value)
81
97
 
82
- def write_multi_command(self, comm_addr: int, offset: int, values: bytes) -> ProtocolCommand:
98
+ def write_multi_command(self, offset: int, values: bytes) -> ProtocolCommand:
83
99
  """Create write multiple protocol command."""
84
- return ModbusRtuWriteMultiCommand(comm_addr, offset, values)
100
+ return ModbusRtuWriteMultiCommand(self._comm_addr, offset, values)
85
101
 
86
102
  async def _connect(self) -> None:
87
103
  if not self._transport or self._transport.is_closing():
@@ -100,7 +116,7 @@ class UdpInverterProtocol(InverterProtocol, asyncio.DatagramProtocol):
100
116
  logger.debug("Socket closed with error: %s.", exc)
101
117
  else:
102
118
  logger.debug("Socket closed.")
103
- self._close_transport()
119
+ self.close_transport()
104
120
 
105
121
  def datagram_received(self, data: bytes, addr: Tuple[str, int]) -> None:
106
122
  """On datagram received"""
@@ -108,22 +124,35 @@ class UdpInverterProtocol(InverterProtocol, asyncio.DatagramProtocol):
108
124
  self._timer.cancel()
109
125
  self._timer = None
110
126
  try:
127
+ if self._partial_data:
128
+ logger.debug("Received another response fragment: %s.", data.hex())
129
+ data = self._partial_data + data
111
130
  if self.command.validator(data):
112
- logger.debug("Received: %s", data.hex())
131
+ if self._partial_data:
132
+ logger.debug("Composed fragmented response: %s", data.hex())
133
+ else:
134
+ logger.debug("Received: %s", data.hex())
135
+ self._partial_data = None
113
136
  self.response_future.set_result(data)
114
137
  else:
115
138
  logger.debug("Received invalid response: %s", data.hex())
116
139
  asyncio.get_running_loop().call_soon(self._retry_mechanism)
140
+ except PartialResponseException:
141
+ logger.debug("Received response fragment: %s", data.hex())
142
+ self._partial_data = data
143
+ return
144
+ except asyncio.InvalidStateError:
145
+ logger.debug("Response already handled: %s", data.hex())
117
146
  except RequestRejectedException as ex:
118
147
  logger.debug("Received exception response: %s", data.hex())
119
148
  self.response_future.set_exception(ex)
120
- self._close_transport()
149
+ self.close_transport()
121
150
 
122
151
  def error_received(self, exc: Exception) -> None:
123
152
  """On error received"""
124
153
  logger.debug("Received error: %s", exc)
125
154
  self.response_future.set_exception(exc)
126
- self._close_transport()
155
+ self.close_transport()
127
156
 
128
157
  async def send_request(self, command: ProtocolCommand) -> Future:
129
158
  """Send message via transport"""
@@ -139,9 +168,12 @@ class UdpInverterProtocol(InverterProtocol, asyncio.DatagramProtocol):
139
168
  """Send message via transport"""
140
169
  self.command = command
141
170
  self.response_future = response_future
142
- logger.debug("Sending: %s%s", self.command,
143
- f' - retry #{self._retry}/{self.retries}' if self._retry > 0 else '')
144
- self._transport.sendto(self.command.request)
171
+ payload = command.request_bytes()
172
+ if self._retry > 0:
173
+ logger.debug("Sending: %s - retry #%s/%s", self.command, self._retry, self.retries)
174
+ else:
175
+ logger.debug("Sending: %s", self.command)
176
+ self._transport.sendto(payload)
145
177
  self._timer = asyncio.get_running_loop().call_later(self.timeout, self._retry_mechanism)
146
178
 
147
179
  def _retry_mechanism(self) -> None:
@@ -156,9 +188,9 @@ class UdpInverterProtocol(InverterProtocol, asyncio.DatagramProtocol):
156
188
  else:
157
189
  logger.debug("Max number of retries (%d) reached, request %s failed.", self.retries, self.command)
158
190
  self.response_future.set_exception(MaxRetriesException)
159
- self._close_transport()
191
+ self.close_transport()
160
192
 
161
- def _close_transport(self) -> None:
193
+ def close_transport(self) -> None:
162
194
  if self._transport:
163
195
  try:
164
196
  self._transport.close()
@@ -171,22 +203,22 @@ class UdpInverterProtocol(InverterProtocol, asyncio.DatagramProtocol):
171
203
 
172
204
 
173
205
  class TcpInverterProtocol(InverterProtocol, asyncio.Protocol):
174
- def __init__(self, host: str, port: int, timeout: int = 1, retries: int = 0):
175
- super().__init__(host, port, timeout, retries)
206
+ def __init__(self, host: str, port: int, comm_addr: int, timeout: int = 1, retries: int = 0):
207
+ super().__init__(host, port, comm_addr, timeout, retries)
176
208
  self._transport: asyncio.transports.Transport | None = None
177
209
  self._retry: int = 0
178
210
 
179
- def read_command(self, comm_addr: int, offset: int, count: int) -> ProtocolCommand:
211
+ def read_command(self, offset: int, count: int) -> ProtocolCommand:
180
212
  """Create read protocol command."""
181
- return ModbusTcpReadCommand(comm_addr, offset, count)
213
+ return ModbusTcpReadCommand(self._comm_addr, offset, count)
182
214
 
183
- def write_command(self, comm_addr: int, register: int, value: int) -> ProtocolCommand:
215
+ def write_command(self, register: int, value: int) -> ProtocolCommand:
184
216
  """Create write protocol command."""
185
- return ModbusTcpWriteCommand(comm_addr, register, value)
217
+ return ModbusTcpWriteCommand(self._comm_addr, register, value)
186
218
 
187
- def write_multi_command(self, comm_addr: int, offset: int, values: bytes) -> ProtocolCommand:
219
+ def write_multi_command(self, offset: int, values: bytes) -> ProtocolCommand:
188
220
  """Create write multiple protocol command."""
189
- return ModbusTcpWriteMultiCommand(comm_addr, offset, values)
221
+ return ModbusTcpWriteMultiCommand(self._comm_addr, offset, values)
190
222
 
191
223
  async def _connect(self) -> None:
192
224
  if not self._transport or self._transport.is_closing():
@@ -195,6 +227,14 @@ class TcpInverterProtocol(InverterProtocol, asyncio.Protocol):
195
227
  lambda: self,
196
228
  host=self._host, port=self._port,
197
229
  )
230
+ sock = self._transport.get_extra_info('socket')
231
+ if sock is not None:
232
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
233
+ sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 10)
234
+ sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 10)
235
+ sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 3)
236
+ if platform.system() == 'Windows':
237
+ sock.ioctl(socket.SIO_KEEPALIVE_VALS, (1, 10000, 10000))
198
238
 
199
239
  def connection_made(self, transport: asyncio.DatagramTransport) -> None:
200
240
  """On connection made"""
@@ -203,7 +243,7 @@ class TcpInverterProtocol(InverterProtocol, asyncio.Protocol):
203
243
 
204
244
  def eof_received(self) -> None:
205
245
  logger.debug("EOF received.")
206
- self._close_transport()
246
+ self.close_transport()
207
247
 
208
248
  def connection_lost(self, exc: Optional[Exception]) -> None:
209
249
  """On connection lost"""
@@ -211,31 +251,44 @@ class TcpInverterProtocol(InverterProtocol, asyncio.Protocol):
211
251
  logger.debug("Connection closed with error: %s.", exc)
212
252
  else:
213
253
  logger.debug("Connection closed.")
214
- self._close_transport()
254
+ self.close_transport()
215
255
 
216
256
  def data_received(self, data: bytes) -> None:
217
257
  """On data received"""
218
258
  if self._timer:
219
259
  self._timer.cancel()
220
260
  try:
261
+ if self._partial_data:
262
+ logger.debug("Received another response fragment: %s.", data.hex())
263
+ data = self._partial_data + data
221
264
  if self.command.validator(data):
222
- logger.debug("Received: %s", data.hex())
265
+ if self._partial_data:
266
+ logger.debug("Composed fragmented response: %s", data.hex())
267
+ else:
268
+ logger.debug("Received: %s", data.hex())
223
269
  self._retry = 0
270
+ self._partial_data = None
224
271
  self.response_future.set_result(data)
225
272
  else:
226
273
  logger.debug("Received invalid response: %s", data.hex())
227
274
  self.response_future.set_exception(RequestRejectedException())
228
- self._close_transport()
275
+ self.close_transport()
276
+ except PartialResponseException:
277
+ logger.debug("Received response fragment: %s", data.hex())
278
+ self._partial_data = data
279
+ return
280
+ except asyncio.InvalidStateError:
281
+ logger.debug("Response already handled: %s", data.hex())
229
282
  except RequestRejectedException as ex:
230
283
  logger.debug("Received exception response: %s", data.hex())
231
284
  self.response_future.set_exception(ex)
232
- # self._close_transport()
285
+ # self.close_transport()
233
286
 
234
287
  def error_received(self, exc: Exception) -> None:
235
288
  """On error received"""
236
289
  logger.debug("Received error: %s", exc)
237
290
  self.response_future.set_exception(exc)
238
- self._close_transport()
291
+ self.close_transport()
239
292
 
240
293
  async def send_request(self, command: ProtocolCommand) -> Future:
241
294
  """Send message via transport"""
@@ -253,11 +306,11 @@ class TcpInverterProtocol(InverterProtocol, asyncio.Protocol):
253
306
  self._retry += 1
254
307
  if self._lock and self._lock.locked():
255
308
  self._lock.release()
256
- self._close_transport()
309
+ self.close_transport()
257
310
  return await self.send_request(command)
258
311
  else:
259
312
  return self._max_retries_reached()
260
- except (ConnectionRefusedError, TimeoutError, OSError, asyncio.TimeoutError) as exc:
313
+ except (ConnectionRefusedError, TimeoutError, OSError, asyncio.TimeoutError):
261
314
  if self._retry < self.retries:
262
315
  logger.debug("Connection refused error.")
263
316
  self._retry += 1
@@ -274,9 +327,12 @@ class TcpInverterProtocol(InverterProtocol, asyncio.Protocol):
274
327
  """Send message via transport"""
275
328
  self.command = command
276
329
  self.response_future = response_future
277
- logger.debug("Sending: %s%s", self.command,
278
- f' - retry #{self._retry}/{self.retries}' if self._retry > 0 else '')
279
- self._transport.write(self.command.request)
330
+ payload = command.request_bytes()
331
+ if self._retry > 0:
332
+ logger.debug("Sending: %s - retry #%s/%s", self.command, self._retry, self.retries)
333
+ else:
334
+ logger.debug("Sending: %s", self.command)
335
+ self._transport.write(payload)
280
336
  self._timer = asyncio.get_running_loop().call_later(self.timeout, self._timeout_mechanism)
281
337
 
282
338
  def _timeout_mechanism(self) -> None:
@@ -287,16 +343,16 @@ class TcpInverterProtocol(InverterProtocol, asyncio.Protocol):
287
343
  if self._timer:
288
344
  logger.debug("Failed to receive response to %s in time (%ds).", self.command, self.timeout)
289
345
  self._timer = None
290
- self._close_transport()
346
+ self.close_transport()
291
347
 
292
348
  def _max_retries_reached(self) -> Future:
293
349
  logger.debug("Max number of retries (%d) reached, request %s failed.", self.retries, self.command)
294
- self._close_transport()
350
+ self.close_transport()
295
351
  self.response_future = asyncio.get_running_loop().create_future()
296
352
  self.response_future.set_exception(MaxRetriesException)
297
353
  return self.response_future
298
354
 
299
- def _close_transport(self) -> None:
355
+ def close_transport(self) -> None:
300
356
  if self._transport:
301
357
  try:
302
358
  self._transport.close()
@@ -354,6 +410,10 @@ class ProtocolCommand:
354
410
  def __repr__(self):
355
411
  return self.request.hex()
356
412
 
413
+ def request_bytes(self) -> bytes:
414
+ """Return raw bytes payload, optionally pre-processed"""
415
+ return self.request
416
+
357
417
  def trim_response(self, raw_response: bytes):
358
418
  """Trim raw response from header and checksum data"""
359
419
  return raw_response
@@ -567,6 +627,12 @@ class ModbusTcpProtocolCommand(ProtocolCommand):
567
627
  self.first_address: int = offset
568
628
  self.value = value
569
629
 
630
+ def request_bytes(self) -> bytes:
631
+ """Return raw bytes payload, optionally pre-processed"""
632
+ # Apply sequential Modbus/TCP transaction identifier
633
+ self.request = _next_tx() + self.request[2:]
634
+ return self.request
635
+
570
636
  def trim_response(self, raw_response: bytes):
571
637
  """Trim raw response from header and checksum data"""
572
638
  return raw_response[9:]
goodwe/sensor.py CHANGED
@@ -183,7 +183,7 @@ class Energy(Sensor):
183
183
 
184
184
  def read_value(self, data: ProtocolResponse):
185
185
  value = read_bytes2(data)
186
- return float(value) / 10 if value else None
186
+ return float(value) / 10 if value is not None else None
187
187
 
188
188
 
189
189
  class Energy4(Sensor):
@@ -194,7 +194,7 @@ class Energy4(Sensor):
194
194
 
195
195
  def read_value(self, data: ProtocolResponse):
196
196
  value = read_bytes4(data)
197
- return float(value) / 10 if value else None
197
+ return float(value) / 10 if value is not None else None
198
198
 
199
199
 
200
200
  class Apparent(Sensor):
@@ -910,7 +910,7 @@ def read_temp(buffer: ProtocolResponse, offset: int = None) -> float | None:
910
910
  if offset is not None:
911
911
  buffer.seek(offset)
912
912
  value = int.from_bytes(buffer.read(2), byteorder="big", signed=True)
913
- if value == 32767:
913
+ if value == -1 or value == 32767:
914
914
  return None
915
915
  else:
916
916
  return float(value) / 10
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: goodwe
3
- Version: 0.4.1
3
+ Version: 0.4.2
4
4
  Summary: Read data from GoodWe inverter via local network
5
5
  Home-page: https://github.com/marcelblijleven/goodwe
6
6
  Author: Martin Letenay, Marcel Blijleven
@@ -0,0 +1,16 @@
1
+ goodwe/__init__.py,sha256=0Zwuri1cbJ2Qe24R2rEjDMTZeVtsh21YIx3KlRaXgWg,5742
2
+ goodwe/const.py,sha256=yhWk56YV7k7-MbgfmWEMYNlqeRNLOfOpfTqEfRj6Hp8,7934
3
+ goodwe/dt.py,sha256=oGbkdVHP51KnlwQraKeebmiP6AtJ1S67aLB7euNRIoE,11743
4
+ goodwe/es.py,sha256=iVK8EMCaAJJFihZLntJZ_Eu4sQWoZTVtTROp9mHFG6o,22730
5
+ goodwe/et.py,sha256=CiX-PE7wouDnj1RnPnOyqiNE4FELhOGdyPUOm9VCzUw,43890
6
+ goodwe/exceptions.py,sha256=dKMLxotjoR1ic8OVlw1joIJ4mKWD6oFtUMZ86fNM5ZE,1403
7
+ goodwe/inverter.py,sha256=-eRq6ND-BpLmj6vgYW0K0Oq3WvNcjjScbkalAzPH5ew,10494
8
+ goodwe/modbus.py,sha256=zT3W9ByANPaZd7T0XTqYGBaVo9PEwyg8jus12mRxCPU,8211
9
+ goodwe/model.py,sha256=dWBjMFJMnhZoUdDd9fGT54DERDANz4TirK0Wy8kWMbk,2068
10
+ goodwe/protocol.py,sha256=m4n1VAonXLBswFEjUcvKXEPV2WcOv_-MDMAefpsQ_-g,27703
11
+ goodwe/sensor.py,sha256=buPG8BcgZmRDqaMrLQUACLHB85U134qG6qo_ggsu48A,37679
12
+ goodwe-0.4.2.dist-info/LICENSE,sha256=aZAhk3lRdYT1YZV-IKRHISEcc_KNUmgfuNO3QhRamNM,1073
13
+ goodwe-0.4.2.dist-info/METADATA,sha256=gOkkNodwpHtUf_743Nc7jCKpdxjwX_L5DSr2POJDjs8,3376
14
+ goodwe-0.4.2.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
15
+ goodwe-0.4.2.dist-info/top_level.txt,sha256=kKoiqiVvAxDaDJYMZZQLgHQj9cuWT1MXLfXElTDuf8s,7
16
+ goodwe-0.4.2.dist-info/RECORD,,
@@ -1,16 +0,0 @@
1
- goodwe/__init__.py,sha256=0Zwuri1cbJ2Qe24R2rEjDMTZeVtsh21YIx3KlRaXgWg,5742
2
- goodwe/const.py,sha256=yhWk56YV7k7-MbgfmWEMYNlqeRNLOfOpfTqEfRj6Hp8,7934
3
- goodwe/dt.py,sha256=q8PRs0nVqN4mEhH8243NTbkkBtrGx-n8icwE-BkTN5Q,10460
4
- goodwe/es.py,sha256=gnSla5SGXK3cJag45o9Z2Wd7rwLkjm3xmS-JN1lf5Ck,22545
5
- goodwe/et.py,sha256=qqC-1r_Q2gmWSYEHhqdrXRCyBLjihCoWbTmbtSGtLJs,43517
6
- goodwe/exceptions.py,sha256=I6PHG0GTWgxNrDVZwJZBnyzItRq5eiM6ci23-EEsn1I,1012
7
- goodwe/inverter.py,sha256=3whUY_ZG7A8aDH1HDSgAzgtFqLOEJBvaxkDwSqKYeuM,10395
8
- goodwe/modbus.py,sha256=NUlG_d3usiJFjTRNpp61u23CLVSx8NfRJLWP4DmxpMU,8196
9
- goodwe/model.py,sha256=dWBjMFJMnhZoUdDd9fGT54DERDANz4TirK0Wy8kWMbk,2068
10
- goodwe/protocol.py,sha256=vVDLDlnjOEMkfdz8_h1bupU4K8VUxyPbrzfNytUK5us,24926
11
- goodwe/sensor.py,sha256=9Oo74Qp9vHmV8trfTn3PlGJrp0Ql1vk0U81oCmod-1c,37640
12
- goodwe-0.4.1.dist-info/LICENSE,sha256=aZAhk3lRdYT1YZV-IKRHISEcc_KNUmgfuNO3QhRamNM,1073
13
- goodwe-0.4.1.dist-info/METADATA,sha256=QpFz1_icH-w7_ZLAxUT3Ee927v_9tUJqbuNqnbsYiPQ,3376
14
- goodwe-0.4.1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
15
- goodwe-0.4.1.dist-info/top_level.txt,sha256=kKoiqiVvAxDaDJYMZZQLgHQj9cuWT1MXLfXElTDuf8s,7
16
- goodwe-0.4.1.dist-info/RECORD,,
File without changes