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 +34 -10
- goodwe/protocol.py +48 -45
- {goodwe-0.4.7.dist-info → goodwe-0.4.8.dist-info}/METADATA +1 -1
- {goodwe-0.4.7.dist-info → goodwe-0.4.8.dist-info}/RECORD +7 -7
- {goodwe-0.4.7.dist-info → goodwe-0.4.8.dist-info}/LICENSE +0 -0
- {goodwe-0.4.7.dist-info → goodwe-0.4.8.dist-info}/WHEEL +0 -0
- {goodwe-0.4.7.dist-info → goodwe-0.4.8.dist-info}/top_level.txt +0 -0
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("
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
164
|
-
self.
|
|
165
|
-
self.
|
|
166
|
-
self.
|
|
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.
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
215
|
+
self._timer = asyncio.get_running_loop().call_later(self.timeout, self._timeout_mechanism)
|
|
182
216
|
|
|
183
|
-
def
|
|
184
|
-
"""
|
|
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
|
-
|
|
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
|
-
|
|
191
|
-
self.
|
|
192
|
-
|
|
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,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=
|
|
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=
|
|
10
|
+
goodwe/protocol.py,sha256=2xRo1H53G6T0ANSuYKPK_KTNfCVTctIU2ZHVu-CvMPk,30163
|
|
11
11
|
goodwe/sensor.py,sha256=xeDZIwjJ_176ULrRXVCTYvVXx6o2_pWgS0KuR3PPQdg,38435
|
|
12
|
-
goodwe-0.4.
|
|
13
|
-
goodwe-0.4.
|
|
14
|
-
goodwe-0.4.
|
|
15
|
-
goodwe-0.4.
|
|
16
|
-
goodwe-0.4.
|
|
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
|
|
File without changes
|
|
File without changes
|