goodwe 0.4.7__py3-none-any.whl → 0.4.8__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
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  import logging
4
4
  from typing import Tuple
5
5
 
6
- from .exceptions import InverterError, RequestRejectedException
6
+ from .exceptions import InverterError, RequestFailedException, RequestRejectedException
7
7
  from .inverter import Inverter
8
8
  from .inverter import OperationMode
9
9
  from .inverter import SensorKind as Kind
@@ -69,7 +69,7 @@ class DT(Inverter):
69
69
  lambda data: round(read_voltage(data, 30120) * read_current(data, 30123)),
70
70
  "On-grid L3 Power", "W", Kind.AC),
71
71
  # 30127 reserved
72
- PowerS("active_power", 30128, "Active Power", Kind.AC),
72
+ PowerS("total_inverter_power", 30128, "Total Power", Kind.AC),
73
73
  Integer("work_mode", 30129, "Work Mode code"),
74
74
  Enum2("work_mode_label", 30129, WORK_MODES, "Work Mode"),
75
75
  Long("error_codes", 30130, "Error Codes"),
@@ -78,7 +78,7 @@ class DT(Inverter):
78
78
  Reactive4("reactive_power", 30135, "Reactive Power", Kind.AC),
79
79
  # 30137 reserved
80
80
  # 30138 reserved
81
- # 30139 reserved
81
+ Decimal("power_factor", 30139, 1000, "Power Factor", "", Kind.GRID),
82
82
  # 30140 reserved
83
83
  Temp("temperature", 30141, "Inverter Temperature", Kind.AC),
84
84
  # 30142 reserved
@@ -113,6 +113,12 @@ class DT(Inverter):
113
113
  # 30172 reserved
114
114
  )
115
115
 
116
+ # Inverter's meter data
117
+ # Modbus registers from offset 0x75f4 (30196)
118
+ __all_sensors_meter: Tuple[Sensor, ...] = (
119
+ PowerS("active_power", 30196, "Active Power", Kind.GRID),
120
+ )
121
+
116
122
  # Modbus registers of inverter settings, offsets are modbus register addresses
117
123
  __all_settings: Tuple[Sensor, ...] = (
118
124
  Timestamp("time", 40313, "Inverter time"),
@@ -139,9 +145,12 @@ class DT(Inverter):
139
145
  def __init__(self, host: str, port: int, comm_addr: int = 0, timeout: int = 1, retries: int = 3):
140
146
  super().__init__(host, port, comm_addr if comm_addr else 0x7f, timeout, retries)
141
147
  self._READ_DEVICE_VERSION_INFO: ProtocolCommand = self._read_command(0x7531, 0x0028)
142
- self._READ_DEVICE_RUNNING_DATA: ProtocolCommand = self._read_command(0x7594, 0x0049)
148
+ self._READ_RUNNING_DATA: ProtocolCommand = self._read_command(0x7594, 0x0049)
149
+ self._READ_METER_DATA: ProtocolCommand = self._read_command(0x75f4, 0x01)
143
150
  self._sensors = self.__all_sensors
151
+ self._sensors_meter = self.__all_sensors_meter
144
152
  self._settings: dict[str, Sensor] = {s.id_: s for s in self.__all_settings}
153
+ self._has_meter: bool = True
145
154
 
146
155
  @staticmethod
147
156
  def _single_phase_only(s: Sensor) -> bool:
@@ -160,10 +169,13 @@ class DT(Inverter):
160
169
  self.model_name = response[22:32].decode("ascii").rstrip()
161
170
  except:
162
171
  print("No model name sent from the inverter.")
163
- self.serial_number = self._decode(response[6:22])
164
- self.dsp1_version = read_unsigned_int(response, 66)
165
- self.dsp2_version = read_unsigned_int(response, 68)
166
- self.arm_version = read_unsigned_int(response, 70)
172
+ # Modbus registers from 30001 - 30040
173
+ self.serial_number = self._decode(response[6:22]) # 30004 - 30012
174
+ self.dsp1_version = read_unsigned_int(response, 66) # 30034
175
+ self.dsp2_version = read_unsigned_int(response, 68) # 30035
176
+ self.arm_version = read_unsigned_int(response, 70) # 30036
177
+ self.dsp_svn_version = read_unsigned_int(response, 72) # 35037
178
+ self.arm_svn_version = read_unsigned_int(response, 74) # 35038
167
179
  self.firmware = "{}.{}.{:02x}".format(self.dsp1_version, self.dsp2_version, self.arm_version)
168
180
 
169
181
  if is_single_phase(self):
@@ -182,8 +194,17 @@ class DT(Inverter):
182
194
  pass
183
195
 
184
196
  async def read_runtime_data(self) -> Dict[str, Any]:
185
- response = await self._read_from_socket(self._READ_DEVICE_RUNNING_DATA)
197
+ response = await self._read_from_socket(self._READ_RUNNING_DATA)
186
198
  data = self._map_response(response, self._sensors)
199
+
200
+ if self._has_meter:
201
+ try:
202
+ response = await self._read_from_socket(self._READ_METER_DATA)
203
+ data.update(self._map_response(response, self._sensors_meter))
204
+ except (RequestRejectedException, RequestFailedException):
205
+ logger.info("Meter values not supported, disabling further attempts.")
206
+ self._has_meter = False
207
+
187
208
  return data
188
209
 
189
210
  async def read_setting(self, setting_id: str) -> Any:
@@ -263,7 +284,10 @@ class DT(Inverter):
263
284
  raise InverterError("Operation not supported, inverter has no batteries.")
264
285
 
265
286
  def sensors(self) -> Tuple[Sensor, ...]:
266
- return self._sensors
287
+ result = self._sensors
288
+ if self._has_meter:
289
+ result = result + self._sensors_meter
290
+ return result
267
291
 
268
292
  def settings(self) -> Tuple[Sensor, ...]:
269
293
  return tuple(self._settings.values())
goodwe/protocol.py CHANGED
@@ -37,7 +37,7 @@ class InverterProtocol:
37
37
  self._timer: asyncio.TimerHandle | None = None
38
38
  self.timeout: int = timeout
39
39
  self.retries: int = retries
40
- self.keep_alive: bool = True
40
+ self.keep_alive: bool = False
41
41
  self.protocol: asyncio.Protocol | None = None
42
42
  self.response_future: Future | None = None
43
43
  self.command: ProtocolCommand | None = None
@@ -62,6 +62,24 @@ class InverterProtocol:
62
62
  self._close_transport()
63
63
  return self._lock
64
64
 
65
+ def _max_retries_reached(self) -> Future:
66
+ logger.debug("Max number of retries (%d) reached, request %s failed.", self.retries, self.command)
67
+ self._close_transport()
68
+ self.response_future = asyncio.get_running_loop().create_future()
69
+ self.response_future.set_exception(MaxRetriesException)
70
+ return self.response_future
71
+
72
+ def _close_transport(self) -> None:
73
+ if self._transport:
74
+ try:
75
+ self._transport.close()
76
+ except RuntimeError:
77
+ logger.debug("Failed to close transport.")
78
+ self._transport = None
79
+ # Cancel Future on connection lost
80
+ if self.response_future and not self.response_future.done():
81
+ self.response_future.cancel()
82
+
65
83
  async def close(self) -> None:
66
84
  """Close the underlying transport/connection."""
67
85
  raise NotImplementedError()
@@ -133,15 +151,16 @@ class UdpInverterProtocol(InverterProtocol, asyncio.DatagramProtocol):
133
151
  self._partial_missing = 0
134
152
  if self.command.validator(data):
135
153
  logger.debug("Received: %s", data.hex())
154
+ self._retry = 0
136
155
  self.response_future.set_result(data)
137
156
  else:
138
157
  logger.debug("Received invalid response: %s", data.hex())
139
- asyncio.get_running_loop().call_soon(self._retry_mechanism)
158
+ asyncio.get_running_loop().call_soon(self._timeout_mechanism)
140
159
  except PartialResponseException as ex:
141
160
  logger.debug("Received response fragment (%d of %d): %s", ex.length, ex.expected, data.hex())
142
161
  self._partial_data = data
143
162
  self._partial_missing = ex.expected - ex.length
144
- self._timer = asyncio.get_running_loop().call_later(self.timeout, self._retry_mechanism)
163
+ self._timer = asyncio.get_running_loop().call_later(self.timeout, self._timeout_mechanism)
145
164
  except asyncio.InvalidStateError:
146
165
  logger.debug("Response already handled: %s", data.hex())
147
166
  except RequestRejectedException as ex:
@@ -158,13 +177,28 @@ class UdpInverterProtocol(InverterProtocol, asyncio.DatagramProtocol):
158
177
 
159
178
  async def send_request(self, command: ProtocolCommand) -> Future:
160
179
  """Send message via transport"""
161
- async with self._ensure_lock():
180
+ await self._ensure_lock().acquire()
181
+ try:
162
182
  await self._connect()
163
183
  response_future = asyncio.get_running_loop().create_future()
164
- self._retry = 0
165
184
  self._send_request(command, response_future)
166
185
  await response_future
167
186
  return response_future
187
+ except asyncio.CancelledError:
188
+ if self._retry < self.retries:
189
+ self._retry += 1
190
+ if self._lock and self._lock.locked():
191
+ self._lock.release()
192
+ if not self.keep_alive:
193
+ self._close_transport()
194
+ return await self.send_request(command)
195
+ else:
196
+ return self._max_retries_reached()
197
+ finally:
198
+ if self._lock and self._lock.locked():
199
+ self._lock.release()
200
+ if not self.keep_alive:
201
+ self._close_transport()
168
202
 
169
203
  def _send_request(self, command: ProtocolCommand, response_future: Future) -> None:
170
204
  """Send message via transport"""
@@ -178,32 +212,19 @@ class UdpInverterProtocol(InverterProtocol, asyncio.DatagramProtocol):
178
212
  else:
179
213
  logger.debug("Sending: %s", self.command)
180
214
  self._transport.sendto(payload)
181
- self._timer = asyncio.get_running_loop().call_later(self.timeout, self._retry_mechanism)
215
+ self._timer = asyncio.get_running_loop().call_later(self.timeout, self._timeout_mechanism)
182
216
 
183
- def _retry_mechanism(self) -> None:
184
- """Retry mechanism to prevent hanging transport"""
185
- if self.response_future.done():
217
+ def _timeout_mechanism(self) -> None:
218
+ """Timeout mechanism to prevent hanging transport"""
219
+ if self.response_future and self.response_future.done():
186
220
  logger.debug("Response already received.")
187
- elif self._retry < self.retries:
221
+ self._retry = 0
222
+ else:
188
223
  if self._timer:
189
224
  logger.debug("Failed to receive response to %s in time (%ds).", self.command, self.timeout)
190
- self._retry += 1
191
- self._send_request(self.command, self.response_future)
192
- else:
193
- logger.debug("Max number of retries (%d) reached, request %s failed.", self.retries, self.command)
194
- self.response_future.set_exception(MaxRetriesException)
195
- self._close_transport()
196
-
197
- def _close_transport(self) -> None:
198
- if self._transport:
199
- try:
200
- self._transport.close()
201
- except RuntimeError:
202
- logger.debug("Failed to close transport.")
203
- self._transport = None
204
- # Cancel Future on connection close
205
- if self.response_future and not self.response_future.done():
206
- self.response_future.cancel()
225
+ self._timer = None
226
+ if self.response_future and not self.response_future.done():
227
+ self.response_future.cancel()
207
228
 
208
229
  async def close(self):
209
230
  self._close_transport()
@@ -358,24 +379,6 @@ class TcpInverterProtocol(InverterProtocol, asyncio.Protocol):
358
379
  self._timer = None
359
380
  self._close_transport()
360
381
 
361
- def _max_retries_reached(self) -> Future:
362
- logger.debug("Max number of retries (%d) reached, request %s failed.", self.retries, self.command)
363
- self._close_transport()
364
- self.response_future = asyncio.get_running_loop().create_future()
365
- self.response_future.set_exception(MaxRetriesException)
366
- return self.response_future
367
-
368
- def _close_transport(self) -> None:
369
- if self._transport:
370
- try:
371
- self._transport.close()
372
- except RuntimeError:
373
- logger.debug("Failed to close transport.")
374
- self._transport = None
375
- # Cancel Future on connection lost
376
- if self.response_future and not self.response_future.done():
377
- self.response_future.cancel()
378
-
379
382
  async def close(self):
380
383
  await self._ensure_lock().acquire()
381
384
  try:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: goodwe
3
- Version: 0.4.7
3
+ Version: 0.4.8
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
@@ -1,16 +1,16 @@
1
1
  goodwe/__init__.py,sha256=8fFGBBvBpCo6Ew4puTtW0kYo2hVPKUx6z5A-TA4Tbvc,5795
2
2
  goodwe/const.py,sha256=yhWk56YV7k7-MbgfmWEMYNlqeRNLOfOpfTqEfRj6Hp8,7934
3
- goodwe/dt.py,sha256=TxjJ4iqvtRiHGKwlfLoYWblN-COVZ3i48PPk4z4xJcc,12482
3
+ goodwe/dt.py,sha256=IJxLDajuu2psYE5ZpwA2HFJDFdK5JIST6kUqWyRVBko,13663
4
4
  goodwe/es.py,sha256=vvHmxcFykp8nhR1I8p7SF0YcYpvdCKBYacgcolbVHXI,23009
5
5
  goodwe/et.py,sha256=Sdgqj13DXIg36NptkHMKxuP78oo4aUQ_6zlToyt78qI,46002
6
6
  goodwe/exceptions.py,sha256=dKMLxotjoR1ic8OVlw1joIJ4mKWD6oFtUMZ86fNM5ZE,1403
7
7
  goodwe/inverter.py,sha256=86aMJzJjNOr1I_tCF5H6mBwzDTjLbGDKUL2hbi0XSxg,10459
8
8
  goodwe/modbus.py,sha256=Mg_s_v8kbZgqXZM6ZUUxkZx2boAG8LkuDG5OiFKK2X4,8402
9
9
  goodwe/model.py,sha256=OAKfw6ggClgLR9JIdNd7tQ4pnh_7o_UqVdm1KOVsm-Y,2200
10
- goodwe/protocol.py,sha256=gnQ1vV4U_lPpaNq5-jmzJO6ngJEDFVo0jWXVujSyu_0,30083
10
+ goodwe/protocol.py,sha256=2xRo1H53G6T0ANSuYKPK_KTNfCVTctIU2ZHVu-CvMPk,30163
11
11
  goodwe/sensor.py,sha256=xeDZIwjJ_176ULrRXVCTYvVXx6o2_pWgS0KuR3PPQdg,38435
12
- goodwe-0.4.7.dist-info/LICENSE,sha256=aZAhk3lRdYT1YZV-IKRHISEcc_KNUmgfuNO3QhRamNM,1073
13
- goodwe-0.4.7.dist-info/METADATA,sha256=qzKMdlhzJDyLdHfqp5_x7jFZMI2B15mTnBhN_UksJzM,3376
14
- goodwe-0.4.7.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
15
- goodwe-0.4.7.dist-info/top_level.txt,sha256=kKoiqiVvAxDaDJYMZZQLgHQj9cuWT1MXLfXElTDuf8s,7
16
- goodwe-0.4.7.dist-info/RECORD,,
12
+ goodwe-0.4.8.dist-info/LICENSE,sha256=aZAhk3lRdYT1YZV-IKRHISEcc_KNUmgfuNO3QhRamNM,1073
13
+ goodwe-0.4.8.dist-info/METADATA,sha256=kla7IF_7dMZl-uEdQnGqMBXOkOyNlKt9Inwmi4BWCWQ,3376
14
+ goodwe-0.4.8.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
15
+ goodwe-0.4.8.dist-info/top_level.txt,sha256=kKoiqiVvAxDaDJYMZZQLgHQj9cuWT1MXLfXElTDuf8s,7
16
+ goodwe-0.4.8.dist-info/RECORD,,
File without changes