goodwe 0.4.1__py3-none-any.whl → 0.4.3__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 +33 -35
- goodwe/dt.py +40 -13
- goodwe/es.py +7 -6
- goodwe/et.py +22 -16
- goodwe/exceptions.py +14 -0
- goodwe/inverter.py +10 -9
- goodwe/modbus.py +12 -9
- goodwe/protocol.py +117 -33
- goodwe/sensor.py +3 -3
- {goodwe-0.4.1.dist-info → goodwe-0.4.3.dist-info}/METADATA +1 -1
- goodwe-0.4.3.dist-info/RECORD +16 -0
- goodwe-0.4.1.dist-info/RECORD +0 -16
- {goodwe-0.4.1.dist-info → goodwe-0.4.3.dist-info}/LICENSE +0 -0
- {goodwe-0.4.1.dist-info → goodwe-0.4.3.dist-info}/WHEEL +0 -0
- {goodwe-0.4.1.dist-info → goodwe-0.4.3.dist-info}/top_level.txt +0 -0
goodwe/__init__.py
CHANGED
|
@@ -22,9 +22,6 @@ DT_FAMILY = ["DT", "MS", "NS", "XS"]
|
|
|
22
22
|
# Initial discovery command
|
|
23
23
|
DISCOVERY_COMMAND = Aa55ProtocolCommand("010200", "0182")
|
|
24
24
|
|
|
25
|
-
# supported inverter protocols
|
|
26
|
-
_SUPPORTED_PROTOCOLS = [ET, DT, ES]
|
|
27
|
-
|
|
28
25
|
|
|
29
26
|
async def connect(host: str, port: int = GOODWE_UDP_PORT, family: str = None, comm_addr: int = 0, timeout: int = 1,
|
|
30
27
|
retries: int = 3, do_discover: bool = True) -> Inverter:
|
|
@@ -41,7 +38,7 @@ async def connect(host: str, port: int = GOODWE_UDP_PORT, family: str = None, co
|
|
|
41
38
|
|
|
42
39
|
Raise InverterError if unable to contact or recognise supported inverter.
|
|
43
40
|
"""
|
|
44
|
-
if family in ET_FAMILY
|
|
41
|
+
if family in ET_FAMILY:
|
|
45
42
|
inv = ET(host, port, comm_addr, timeout, retries)
|
|
46
43
|
elif family in ES_FAMILY:
|
|
47
44
|
inv = ES(host, port, comm_addr, timeout, retries)
|
|
@@ -65,42 +62,43 @@ async def discover(host: str, port: int = GOODWE_UDP_PORT, timeout: int = 1, ret
|
|
|
65
62
|
"""
|
|
66
63
|
failures = []
|
|
67
64
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
logger.debug("Detected ET/EH/BT/BH/GEH inverter %s, S/N:%s.", model_name, serial_number)
|
|
80
|
-
i = ET(host, port, 0, timeout, retries)
|
|
81
|
-
break
|
|
82
|
-
if not i:
|
|
83
|
-
for model_tag in ES_MODEL_TAGS:
|
|
84
|
-
if model_tag in serial_number:
|
|
85
|
-
logger.debug("Detected ES/EM/BP inverter %s, S/N:%s.", model_name, serial_number)
|
|
86
|
-
i = ES(host, port, 0, timeout, retries)
|
|
87
|
-
break
|
|
88
|
-
if not i:
|
|
89
|
-
for model_tag in DT_MODEL_TAGS:
|
|
65
|
+
if port == GOODWE_UDP_PORT:
|
|
66
|
+
# Try the common AA55C07F0102000241 command first and detect inverter type from serial_number
|
|
67
|
+
try:
|
|
68
|
+
logger.debug("Probing inverter at %s:%s.", host, port)
|
|
69
|
+
response = await DISCOVERY_COMMAND.execute(UdpInverterProtocol(host, port, timeout, retries))
|
|
70
|
+
response = response.response_data()
|
|
71
|
+
model_name = response[5:15].decode("ascii").rstrip()
|
|
72
|
+
serial_number = response[31:47].decode("ascii")
|
|
73
|
+
|
|
74
|
+
i: Inverter | None = None
|
|
75
|
+
for model_tag in ET_MODEL_TAGS:
|
|
90
76
|
if model_tag in serial_number:
|
|
91
|
-
logger.debug("Detected
|
|
92
|
-
i =
|
|
77
|
+
logger.debug("Detected ET/EH/BT/BH/GEH inverter %s, S/N:%s.", model_name, serial_number)
|
|
78
|
+
i = ET(host, port, 0, timeout, retries)
|
|
93
79
|
break
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
80
|
+
if not i:
|
|
81
|
+
for model_tag in ES_MODEL_TAGS:
|
|
82
|
+
if model_tag in serial_number:
|
|
83
|
+
logger.debug("Detected ES/EM/BP inverter %s, S/N:%s.", model_name, serial_number)
|
|
84
|
+
i = ES(host, port, 0, timeout, retries)
|
|
85
|
+
break
|
|
86
|
+
if not i:
|
|
87
|
+
for model_tag in DT_MODEL_TAGS:
|
|
88
|
+
if model_tag in serial_number:
|
|
89
|
+
logger.debug("Detected DT/MS/D-NS/XS/GEP inverter %s, S/N:%s.", model_name, serial_number)
|
|
90
|
+
i = DT(host, port, 0, timeout, retries)
|
|
91
|
+
break
|
|
92
|
+
if i:
|
|
93
|
+
await i.read_device_info()
|
|
94
|
+
logger.debug("Connected to inverter %s, S/N:%s.", i.model_name, i.serial_number)
|
|
95
|
+
return i
|
|
98
96
|
|
|
99
|
-
|
|
100
|
-
|
|
97
|
+
except InverterError as ex:
|
|
98
|
+
failures.append(ex)
|
|
101
99
|
|
|
102
100
|
# Probe inverter specific protocols
|
|
103
|
-
for inv in
|
|
101
|
+
for inv in [ET, DT, ES]:
|
|
104
102
|
i = inv(host, port, 0, timeout, retries)
|
|
105
103
|
try:
|
|
106
104
|
logger.debug("Probing %s inverter at %s.", inv.__name__, host)
|
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
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
632
|
-
|
|
633
|
-
|
|
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,11 +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
94
|
|
|
95
|
-
self.comm_addr: int = comm_addr
|
|
96
|
-
|
|
97
95
|
self.model_name: str | None = None
|
|
98
96
|
self.serial_number: str | None = None
|
|
99
97
|
self.rated_power: int = 0
|
|
@@ -109,15 +107,15 @@ class Inverter(ABC):
|
|
|
109
107
|
|
|
110
108
|
def _read_command(self, offset: int, count: int) -> ProtocolCommand:
|
|
111
109
|
"""Create read protocol command."""
|
|
112
|
-
return self._protocol.read_command(
|
|
110
|
+
return self._protocol.read_command(offset, count)
|
|
113
111
|
|
|
114
112
|
def _write_command(self, register: int, value: int) -> ProtocolCommand:
|
|
115
113
|
"""Create write protocol command."""
|
|
116
|
-
return self._protocol.write_command(
|
|
114
|
+
return self._protocol.write_command(register, value)
|
|
117
115
|
|
|
118
116
|
def _write_multi_command(self, offset: int, values: bytes) -> ProtocolCommand:
|
|
119
117
|
"""Create write multiple protocol command."""
|
|
120
|
-
return self._protocol.write_multi_command(
|
|
118
|
+
return self._protocol.write_multi_command(offset, values)
|
|
121
119
|
|
|
122
120
|
async def _read_from_socket(self, command: ProtocolCommand) -> ProtocolResponse:
|
|
123
121
|
try:
|
|
@@ -132,6 +130,9 @@ class Inverter(ABC):
|
|
|
132
130
|
self._consecutive_failures_count += 1
|
|
133
131
|
raise RequestFailedException(ex.message, self._consecutive_failures_count) from None
|
|
134
132
|
|
|
133
|
+
def set_keep_alive(self, keep_alive: bool) -> None:
|
|
134
|
+
self._protocol.keep_alive = keep_alive
|
|
135
|
+
|
|
135
136
|
@abstractmethod
|
|
136
137
|
async def read_device_info(self):
|
|
137
138
|
"""
|
|
@@ -270,11 +271,11 @@ class Inverter(ABC):
|
|
|
270
271
|
raise NotImplementedError()
|
|
271
272
|
|
|
272
273
|
@staticmethod
|
|
273
|
-
def _create_protocol(host: str, port: int, timeout: int, retries: int) -> InverterProtocol:
|
|
274
|
+
def _create_protocol(host: str, port: int, comm_addr: int, timeout: int, retries: int) -> InverterProtocol:
|
|
274
275
|
if port == 502:
|
|
275
|
-
return TcpInverterProtocol(host, port, timeout, retries)
|
|
276
|
+
return TcpInverterProtocol(host, port, comm_addr, timeout, retries)
|
|
276
277
|
else:
|
|
277
|
-
return UdpInverterProtocol(host, port, timeout, retries)
|
|
278
|
+
return UdpInverterProtocol(host, port, comm_addr, timeout, retries)
|
|
278
279
|
|
|
279
280
|
@staticmethod
|
|
280
281
|
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
|
-
|
|
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,22 @@ 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
|
+
# The weird expected_length != 12 is work around Goodwe bug answering wrong (hardcoded 6) length.
|
|
226
|
+
if len(data) < expected_length and expected_length != 12:
|
|
227
|
+
raise PartialResponseException(len(data), expected_length)
|
|
228
|
+
|
|
225
229
|
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
230
|
expected_length = data[8] + 9
|
|
230
231
|
if len(data) < expected_length:
|
|
231
|
-
|
|
232
|
+
raise PartialResponseException(len(data), expected_length)
|
|
233
|
+
if data[8] != value * 2:
|
|
234
|
+
logger.debug("Response has unexpected length: %d, expected %d.", data[8], value * 2)
|
|
232
235
|
return False
|
|
233
236
|
elif data[7] in (MODBUS_WRITE_CMD, MODBUS_WRITE_MULTI_CMD):
|
|
234
237
|
if len(data) < 12:
|
|
235
238
|
logger.debug("Response has unexpected length: %d, expected %d.", len(data), 14)
|
|
236
|
-
|
|
239
|
+
raise PartialResponseException(len(data), expected_length)
|
|
237
240
|
response_offset = int.from_bytes(data[8:10], byteorder='big', signed=False)
|
|
238
241
|
if response_offset != offset:
|
|
239
242
|
logger.debug("Response has wrong offset: %X, expected %X.", response_offset, offset)
|
goodwe/protocol.py
CHANGED
|
@@ -3,30 +3,45 @@ 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
|
|
25
38
|
self.timeout: int = timeout
|
|
26
39
|
self.retries: int = retries
|
|
40
|
+
self.keep_alive: bool = True
|
|
27
41
|
self.protocol: asyncio.Protocol | None = None
|
|
28
42
|
self.response_future: Future | None = None
|
|
29
43
|
self.command: ProtocolCommand | None = None
|
|
44
|
+
self._partial_data: bytes | None = None
|
|
30
45
|
|
|
31
46
|
def _ensure_lock(self) -> asyncio.Lock:
|
|
32
47
|
"""Validate (or create) asyncio Lock.
|
|
@@ -46,42 +61,44 @@ class InverterProtocol:
|
|
|
46
61
|
self._close_transport()
|
|
47
62
|
return self._lock
|
|
48
63
|
|
|
49
|
-
def
|
|
64
|
+
async def close(self) -> None:
|
|
65
|
+
"""Close the underlying transport/connection."""
|
|
50
66
|
raise NotImplementedError()
|
|
51
67
|
|
|
52
68
|
async def send_request(self, command: ProtocolCommand) -> Future:
|
|
69
|
+
"""Convert command to request and send it to inverter."""
|
|
53
70
|
raise NotImplementedError()
|
|
54
71
|
|
|
55
|
-
def read_command(self,
|
|
72
|
+
def read_command(self, offset: int, count: int) -> ProtocolCommand:
|
|
56
73
|
"""Create read protocol command."""
|
|
57
74
|
raise NotImplementedError()
|
|
58
75
|
|
|
59
|
-
def write_command(self,
|
|
76
|
+
def write_command(self, register: int, value: int) -> ProtocolCommand:
|
|
60
77
|
"""Create write protocol command."""
|
|
61
78
|
raise NotImplementedError()
|
|
62
79
|
|
|
63
|
-
def write_multi_command(self,
|
|
80
|
+
def write_multi_command(self, offset: int, values: bytes) -> ProtocolCommand:
|
|
64
81
|
"""Create write multiple protocol command."""
|
|
65
82
|
raise NotImplementedError()
|
|
66
83
|
|
|
67
84
|
|
|
68
85
|
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)
|
|
86
|
+
def __init__(self, host: str, port: int, comm_addr: int, timeout: int = 1, retries: int = 3):
|
|
87
|
+
super().__init__(host, port, comm_addr, timeout, retries)
|
|
71
88
|
self._transport: asyncio.transports.DatagramTransport | None = None
|
|
72
89
|
self._retry: int = 0
|
|
73
90
|
|
|
74
|
-
def read_command(self,
|
|
91
|
+
def read_command(self, offset: int, count: int) -> ProtocolCommand:
|
|
75
92
|
"""Create read protocol command."""
|
|
76
|
-
return ModbusRtuReadCommand(
|
|
93
|
+
return ModbusRtuReadCommand(self._comm_addr, offset, count)
|
|
77
94
|
|
|
78
|
-
def write_command(self,
|
|
95
|
+
def write_command(self, register: int, value: int) -> ProtocolCommand:
|
|
79
96
|
"""Create write protocol command."""
|
|
80
|
-
return ModbusRtuWriteCommand(
|
|
97
|
+
return ModbusRtuWriteCommand(self._comm_addr, register, value)
|
|
81
98
|
|
|
82
|
-
def write_multi_command(self,
|
|
99
|
+
def write_multi_command(self, offset: int, values: bytes) -> ProtocolCommand:
|
|
83
100
|
"""Create write multiple protocol command."""
|
|
84
|
-
return ModbusRtuWriteMultiCommand(
|
|
101
|
+
return ModbusRtuWriteMultiCommand(self._comm_addr, offset, values)
|
|
85
102
|
|
|
86
103
|
async def _connect(self) -> None:
|
|
87
104
|
if not self._transport or self._transport.is_closing():
|
|
@@ -108,12 +125,25 @@ class UdpInverterProtocol(InverterProtocol, asyncio.DatagramProtocol):
|
|
|
108
125
|
self._timer.cancel()
|
|
109
126
|
self._timer = None
|
|
110
127
|
try:
|
|
128
|
+
if self._partial_data:
|
|
129
|
+
logger.debug("Received another response fragment: %s.", data.hex())
|
|
130
|
+
data = self._partial_data + data
|
|
111
131
|
if self.command.validator(data):
|
|
112
|
-
|
|
132
|
+
if self._partial_data:
|
|
133
|
+
logger.debug("Composed fragmented response: %s", data.hex())
|
|
134
|
+
else:
|
|
135
|
+
logger.debug("Received: %s", data.hex())
|
|
136
|
+
self._partial_data = None
|
|
113
137
|
self.response_future.set_result(data)
|
|
114
138
|
else:
|
|
115
139
|
logger.debug("Received invalid response: %s", data.hex())
|
|
116
140
|
asyncio.get_running_loop().call_soon(self._retry_mechanism)
|
|
141
|
+
except PartialResponseException:
|
|
142
|
+
logger.debug("Received response fragment: %s", data.hex())
|
|
143
|
+
self._partial_data = data
|
|
144
|
+
return
|
|
145
|
+
except asyncio.InvalidStateError:
|
|
146
|
+
logger.debug("Response already handled: %s", data.hex())
|
|
117
147
|
except RequestRejectedException as ex:
|
|
118
148
|
logger.debug("Received exception response: %s", data.hex())
|
|
119
149
|
self.response_future.set_exception(ex)
|
|
@@ -139,9 +169,12 @@ class UdpInverterProtocol(InverterProtocol, asyncio.DatagramProtocol):
|
|
|
139
169
|
"""Send message via transport"""
|
|
140
170
|
self.command = command
|
|
141
171
|
self.response_future = response_future
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
172
|
+
payload = command.request_bytes()
|
|
173
|
+
if self._retry > 0:
|
|
174
|
+
logger.debug("Sending: %s - retry #%s/%s", self.command, self._retry, self.retries)
|
|
175
|
+
else:
|
|
176
|
+
logger.debug("Sending: %s", self.command)
|
|
177
|
+
self._transport.sendto(payload)
|
|
145
178
|
self._timer = asyncio.get_running_loop().call_later(self.timeout, self._retry_mechanism)
|
|
146
179
|
|
|
147
180
|
def _retry_mechanism(self) -> None:
|
|
@@ -169,24 +202,27 @@ class UdpInverterProtocol(InverterProtocol, asyncio.DatagramProtocol):
|
|
|
169
202
|
if self.response_future and not self.response_future.done():
|
|
170
203
|
self.response_future.cancel()
|
|
171
204
|
|
|
205
|
+
async def close(self):
|
|
206
|
+
self._close_transport()
|
|
207
|
+
|
|
172
208
|
|
|
173
209
|
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)
|
|
210
|
+
def __init__(self, host: str, port: int, comm_addr: int, timeout: int = 1, retries: int = 0):
|
|
211
|
+
super().__init__(host, port, comm_addr, timeout, retries)
|
|
176
212
|
self._transport: asyncio.transports.Transport | None = None
|
|
177
213
|
self._retry: int = 0
|
|
178
214
|
|
|
179
|
-
def read_command(self,
|
|
215
|
+
def read_command(self, offset: int, count: int) -> ProtocolCommand:
|
|
180
216
|
"""Create read protocol command."""
|
|
181
|
-
return ModbusTcpReadCommand(
|
|
217
|
+
return ModbusTcpReadCommand(self._comm_addr, offset, count)
|
|
182
218
|
|
|
183
|
-
def write_command(self,
|
|
219
|
+
def write_command(self, register: int, value: int) -> ProtocolCommand:
|
|
184
220
|
"""Create write protocol command."""
|
|
185
|
-
return ModbusTcpWriteCommand(
|
|
221
|
+
return ModbusTcpWriteCommand(self._comm_addr, register, value)
|
|
186
222
|
|
|
187
|
-
def write_multi_command(self,
|
|
223
|
+
def write_multi_command(self, offset: int, values: bytes) -> ProtocolCommand:
|
|
188
224
|
"""Create write multiple protocol command."""
|
|
189
|
-
return ModbusTcpWriteMultiCommand(
|
|
225
|
+
return ModbusTcpWriteMultiCommand(self._comm_addr, offset, values)
|
|
190
226
|
|
|
191
227
|
async def _connect(self) -> None:
|
|
192
228
|
if not self._transport or self._transport.is_closing():
|
|
@@ -195,6 +231,18 @@ class TcpInverterProtocol(InverterProtocol, asyncio.Protocol):
|
|
|
195
231
|
lambda: self,
|
|
196
232
|
host=self._host, port=self._port,
|
|
197
233
|
)
|
|
234
|
+
if self.keep_alive:
|
|
235
|
+
try:
|
|
236
|
+
sock = self._transport.get_extra_info('socket')
|
|
237
|
+
if sock is not None:
|
|
238
|
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
|
239
|
+
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 10)
|
|
240
|
+
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 10)
|
|
241
|
+
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 3)
|
|
242
|
+
if platform.system() == 'Windows':
|
|
243
|
+
sock.ioctl(socket.SIO_KEEPALIVE_VALS, (1, 10000, 10000))
|
|
244
|
+
except AttributeError as ex:
|
|
245
|
+
logger.debug("Failed to apply KEEPALIVE: %s", ex)
|
|
198
246
|
|
|
199
247
|
def connection_made(self, transport: asyncio.DatagramTransport) -> None:
|
|
200
248
|
"""On connection made"""
|
|
@@ -218,14 +266,27 @@ class TcpInverterProtocol(InverterProtocol, asyncio.Protocol):
|
|
|
218
266
|
if self._timer:
|
|
219
267
|
self._timer.cancel()
|
|
220
268
|
try:
|
|
269
|
+
if self._partial_data:
|
|
270
|
+
logger.debug("Received another response fragment: %s.", data.hex())
|
|
271
|
+
data = self._partial_data + data
|
|
221
272
|
if self.command.validator(data):
|
|
222
|
-
|
|
273
|
+
if self._partial_data:
|
|
274
|
+
logger.debug("Composed fragmented response: %s", data.hex())
|
|
275
|
+
else:
|
|
276
|
+
logger.debug("Received: %s", data.hex())
|
|
223
277
|
self._retry = 0
|
|
278
|
+
self._partial_data = None
|
|
224
279
|
self.response_future.set_result(data)
|
|
225
280
|
else:
|
|
226
281
|
logger.debug("Received invalid response: %s", data.hex())
|
|
227
282
|
self.response_future.set_exception(RequestRejectedException())
|
|
228
283
|
self._close_transport()
|
|
284
|
+
except PartialResponseException:
|
|
285
|
+
logger.debug("Received response fragment: %s", data.hex())
|
|
286
|
+
self._partial_data = data
|
|
287
|
+
return
|
|
288
|
+
except asyncio.InvalidStateError:
|
|
289
|
+
logger.debug("Response already handled: %s", data.hex())
|
|
229
290
|
except RequestRejectedException as ex:
|
|
230
291
|
logger.debug("Received exception response: %s", data.hex())
|
|
231
292
|
self.response_future.set_exception(ex)
|
|
@@ -257,7 +318,7 @@ class TcpInverterProtocol(InverterProtocol, asyncio.Protocol):
|
|
|
257
318
|
return await self.send_request(command)
|
|
258
319
|
else:
|
|
259
320
|
return self._max_retries_reached()
|
|
260
|
-
except (ConnectionRefusedError, TimeoutError, OSError, asyncio.TimeoutError)
|
|
321
|
+
except (ConnectionRefusedError, TimeoutError, OSError, asyncio.TimeoutError):
|
|
261
322
|
if self._retry < self.retries:
|
|
262
323
|
logger.debug("Connection refused error.")
|
|
263
324
|
self._retry += 1
|
|
@@ -274,9 +335,12 @@ class TcpInverterProtocol(InverterProtocol, asyncio.Protocol):
|
|
|
274
335
|
"""Send message via transport"""
|
|
275
336
|
self.command = command
|
|
276
337
|
self.response_future = response_future
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
338
|
+
payload = command.request_bytes()
|
|
339
|
+
if self._retry > 0:
|
|
340
|
+
logger.debug("Sending: %s - retry #%s/%s", self.command, self._retry, self.retries)
|
|
341
|
+
else:
|
|
342
|
+
logger.debug("Sending: %s", self.command)
|
|
343
|
+
self._transport.write(payload)
|
|
280
344
|
self._timer = asyncio.get_running_loop().call_later(self.timeout, self._timeout_mechanism)
|
|
281
345
|
|
|
282
346
|
def _timeout_mechanism(self) -> None:
|
|
@@ -307,6 +371,14 @@ class TcpInverterProtocol(InverterProtocol, asyncio.Protocol):
|
|
|
307
371
|
if self.response_future and not self.response_future.done():
|
|
308
372
|
self.response_future.cancel()
|
|
309
373
|
|
|
374
|
+
async def close(self):
|
|
375
|
+
await self._ensure_lock().acquire()
|
|
376
|
+
try:
|
|
377
|
+
self._close_transport()
|
|
378
|
+
finally:
|
|
379
|
+
if self._lock and self._lock.locked():
|
|
380
|
+
self._lock.release()
|
|
381
|
+
|
|
310
382
|
|
|
311
383
|
class ProtocolResponse:
|
|
312
384
|
"""Definition of response to protocol command"""
|
|
@@ -354,6 +426,10 @@ class ProtocolCommand:
|
|
|
354
426
|
def __repr__(self):
|
|
355
427
|
return self.request.hex()
|
|
356
428
|
|
|
429
|
+
def request_bytes(self) -> bytes:
|
|
430
|
+
"""Return raw bytes payload, optionally pre-processed"""
|
|
431
|
+
return self.request
|
|
432
|
+
|
|
357
433
|
def trim_response(self, raw_response: bytes):
|
|
358
434
|
"""Trim raw response from header and checksum data"""
|
|
359
435
|
return raw_response
|
|
@@ -381,6 +457,9 @@ class ProtocolCommand:
|
|
|
381
457
|
raise RequestFailedException(
|
|
382
458
|
"No valid response received to '" + self.request.hex() + "' request."
|
|
383
459
|
) from None
|
|
460
|
+
finally:
|
|
461
|
+
if not protocol.keep_alive:
|
|
462
|
+
await protocol.close()
|
|
384
463
|
|
|
385
464
|
|
|
386
465
|
class Aa55ProtocolCommand(ProtocolCommand):
|
|
@@ -425,8 +504,7 @@ class Aa55ProtocolCommand(ProtocolCommand):
|
|
|
425
504
|
data[-2:] is checksum (plain sum of response data incl. header)
|
|
426
505
|
"""
|
|
427
506
|
if len(data) <= 8 or len(data) != data[6] + 9:
|
|
428
|
-
|
|
429
|
-
return False
|
|
507
|
+
raise PartialResponseException(len(data), data[6] + 9)
|
|
430
508
|
elif response_type:
|
|
431
509
|
data_rt_int = int.from_bytes(data[4:6], byteorder="big", signed=True)
|
|
432
510
|
if int(response_type, 16) != data_rt_int:
|
|
@@ -567,6 +645,12 @@ class ModbusTcpProtocolCommand(ProtocolCommand):
|
|
|
567
645
|
self.first_address: int = offset
|
|
568
646
|
self.value = value
|
|
569
647
|
|
|
648
|
+
def request_bytes(self) -> bytes:
|
|
649
|
+
"""Return raw bytes payload, optionally pre-processed"""
|
|
650
|
+
# Apply sequential Modbus/TCP transaction identifier
|
|
651
|
+
self.request = _next_tx() + self.request[2:]
|
|
652
|
+
return self.request
|
|
653
|
+
|
|
570
654
|
def trim_response(self, raw_response: bytes):
|
|
571
655
|
"""Trim raw response from header and checksum data"""
|
|
572
656
|
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
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
goodwe/__init__.py,sha256=8fFGBBvBpCo6Ew4puTtW0kYo2hVPKUx6z5A-TA4Tbvc,5795
|
|
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=86aMJzJjNOr1I_tCF5H6mBwzDTjLbGDKUL2hbi0XSxg,10459
|
|
8
|
+
goodwe/modbus.py,sha256=qDFs8pMOtwgHPfwiZLd-P34vCLHc71-b8MQZMb8FJME,8488
|
|
9
|
+
goodwe/model.py,sha256=dWBjMFJMnhZoUdDd9fGT54DERDANz4TirK0Wy8kWMbk,2068
|
|
10
|
+
goodwe/protocol.py,sha256=JhWYzUtCwbhxXCfZMA_hPGGCHcEEhn0y9B4goJ2GyNo,28306
|
|
11
|
+
goodwe/sensor.py,sha256=buPG8BcgZmRDqaMrLQUACLHB85U134qG6qo_ggsu48A,37679
|
|
12
|
+
goodwe-0.4.3.dist-info/LICENSE,sha256=aZAhk3lRdYT1YZV-IKRHISEcc_KNUmgfuNO3QhRamNM,1073
|
|
13
|
+
goodwe-0.4.3.dist-info/METADATA,sha256=m3IqLUDf7Ae-ElSqvqJTNFmUu-ZaQWr1YWmjz7eqJaw,3376
|
|
14
|
+
goodwe-0.4.3.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
15
|
+
goodwe-0.4.3.dist-info/top_level.txt,sha256=kKoiqiVvAxDaDJYMZZQLgHQj9cuWT1MXLfXElTDuf8s,7
|
|
16
|
+
goodwe-0.4.3.dist-info/RECORD,,
|
goodwe-0.4.1.dist-info/RECORD
DELETED
|
@@ -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
|
|
File without changes
|
|
File without changes
|