goodwe 0.4.6__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 +40 -10
- goodwe/et.py +35 -3
- goodwe/model.py +4 -0
- goodwe/protocol.py +48 -45
- goodwe/sensor.py +19 -0
- {goodwe-0.4.6.dist-info → goodwe-0.4.8.dist-info}/METADATA +1 -1
- goodwe-0.4.8.dist-info/RECORD +16 -0
- goodwe-0.4.6.dist-info/RECORD +0 -16
- {goodwe-0.4.6.dist-info → goodwe-0.4.8.dist-info}/LICENSE +0 -0
- {goodwe-0.4.6.dist-info → goodwe-0.4.8.dist-info}/WHEEL +0 -0
- {goodwe-0.4.6.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
|
|
@@ -35,6 +35,12 @@ class DT(Inverter):
|
|
|
35
35
|
Calculated("ppv3",
|
|
36
36
|
lambda data: round(read_voltage(data, 30107) * read_current(data, 30108)),
|
|
37
37
|
"PV3 Power", "W", Kind.PV),
|
|
38
|
+
# ppv1 + ppv2 + ppv3
|
|
39
|
+
Calculated("ppv",
|
|
40
|
+
lambda data: (round(read_voltage(data, 30103) * read_current(data, 30104))) + (round(
|
|
41
|
+
read_voltage(data, 30105) * read_current(data, 30106))) + (round(
|
|
42
|
+
read_voltage(data, 30107) * read_current(data, 30108))),
|
|
43
|
+
"PV Power", "W", Kind.PV),
|
|
38
44
|
# Voltage("vpv4", 14, "PV4 Voltage", Kind.PV),
|
|
39
45
|
# Current("ipv4", 16, "PV4 Current", Kind.PV),
|
|
40
46
|
# Voltage("vpv5", 14, "PV5 Voltage", Kind.PV),
|
|
@@ -63,7 +69,7 @@ class DT(Inverter):
|
|
|
63
69
|
lambda data: round(read_voltage(data, 30120) * read_current(data, 30123)),
|
|
64
70
|
"On-grid L3 Power", "W", Kind.AC),
|
|
65
71
|
# 30127 reserved
|
|
66
|
-
|
|
72
|
+
PowerS("total_inverter_power", 30128, "Total Power", Kind.AC),
|
|
67
73
|
Integer("work_mode", 30129, "Work Mode code"),
|
|
68
74
|
Enum2("work_mode_label", 30129, WORK_MODES, "Work Mode"),
|
|
69
75
|
Long("error_codes", 30130, "Error Codes"),
|
|
@@ -72,7 +78,7 @@ class DT(Inverter):
|
|
|
72
78
|
Reactive4("reactive_power", 30135, "Reactive Power", Kind.AC),
|
|
73
79
|
# 30137 reserved
|
|
74
80
|
# 30138 reserved
|
|
75
|
-
|
|
81
|
+
Decimal("power_factor", 30139, 1000, "Power Factor", "", Kind.GRID),
|
|
76
82
|
# 30140 reserved
|
|
77
83
|
Temp("temperature", 30141, "Inverter Temperature", Kind.AC),
|
|
78
84
|
# 30142 reserved
|
|
@@ -107,6 +113,12 @@ class DT(Inverter):
|
|
|
107
113
|
# 30172 reserved
|
|
108
114
|
)
|
|
109
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
|
+
|
|
110
122
|
# Modbus registers of inverter settings, offsets are modbus register addresses
|
|
111
123
|
__all_settings: Tuple[Sensor, ...] = (
|
|
112
124
|
Timestamp("time", 40313, "Inverter time"),
|
|
@@ -133,9 +145,12 @@ class DT(Inverter):
|
|
|
133
145
|
def __init__(self, host: str, port: int, comm_addr: int = 0, timeout: int = 1, retries: int = 3):
|
|
134
146
|
super().__init__(host, port, comm_addr if comm_addr else 0x7f, timeout, retries)
|
|
135
147
|
self._READ_DEVICE_VERSION_INFO: ProtocolCommand = self._read_command(0x7531, 0x0028)
|
|
136
|
-
self.
|
|
148
|
+
self._READ_RUNNING_DATA: ProtocolCommand = self._read_command(0x7594, 0x0049)
|
|
149
|
+
self._READ_METER_DATA: ProtocolCommand = self._read_command(0x75f4, 0x01)
|
|
137
150
|
self._sensors = self.__all_sensors
|
|
151
|
+
self._sensors_meter = self.__all_sensors_meter
|
|
138
152
|
self._settings: dict[str, Sensor] = {s.id_: s for s in self.__all_settings}
|
|
153
|
+
self._has_meter: bool = True
|
|
139
154
|
|
|
140
155
|
@staticmethod
|
|
141
156
|
def _single_phase_only(s: Sensor) -> bool:
|
|
@@ -154,10 +169,13 @@ class DT(Inverter):
|
|
|
154
169
|
self.model_name = response[22:32].decode("ascii").rstrip()
|
|
155
170
|
except:
|
|
156
171
|
print("No model name sent from the inverter.")
|
|
157
|
-
|
|
158
|
-
self.
|
|
159
|
-
self.
|
|
160
|
-
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
|
|
161
179
|
self.firmware = "{}.{}.{:02x}".format(self.dsp1_version, self.dsp2_version, self.arm_version)
|
|
162
180
|
|
|
163
181
|
if is_single_phase(self):
|
|
@@ -176,8 +194,17 @@ class DT(Inverter):
|
|
|
176
194
|
pass
|
|
177
195
|
|
|
178
196
|
async def read_runtime_data(self) -> Dict[str, Any]:
|
|
179
|
-
response = await self._read_from_socket(self.
|
|
197
|
+
response = await self._read_from_socket(self._READ_RUNNING_DATA)
|
|
180
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
|
+
|
|
181
208
|
return data
|
|
182
209
|
|
|
183
210
|
async def read_setting(self, setting_id: str) -> Any:
|
|
@@ -257,7 +284,10 @@ class DT(Inverter):
|
|
|
257
284
|
raise InverterError("Operation not supported, inverter has no batteries.")
|
|
258
285
|
|
|
259
286
|
def sensors(self) -> Tuple[Sensor, ...]:
|
|
260
|
-
|
|
287
|
+
result = self._sensors
|
|
288
|
+
if self._has_meter:
|
|
289
|
+
result = result + self._sensors_meter
|
|
290
|
+
return result
|
|
261
291
|
|
|
262
292
|
def settings(self) -> Tuple[Sensor, ...]:
|
|
263
293
|
return tuple(self._settings.values())
|
goodwe/et.py
CHANGED
|
@@ -257,7 +257,8 @@ class ET(Inverter):
|
|
|
257
257
|
Apparent4("meter_apparent_power_total", 36041, "Meter Apparent Power Total", Kind.GRID),
|
|
258
258
|
Integer("meter_type", 36043, "Meter Type", "", Kind.GRID), # (0: Single phase, 1: 3P3W, 2: 3P4W, 3: HomeKit)
|
|
259
259
|
Integer("meter_sw_version", 36044, "Meter Software Version", "", Kind.GRID),
|
|
260
|
-
|
|
260
|
+
|
|
261
|
+
# Sensors added in some ARM fw update (or platform 745/753), read when flag _has_meter_extended is on
|
|
261
262
|
Power4S("meter2_active_power", 36045, "Meter 2 Active Power", Kind.GRID),
|
|
262
263
|
Float("meter2_e_total_exp", 36047, 1000, "Meter 2 Total Energy (export)", "kWh", Kind.GRID),
|
|
263
264
|
Float("meter2_e_total_imp", 36049, 1000, "Meter 2 Total Energy (import)", "kWh", Kind.GRID),
|
|
@@ -268,6 +269,15 @@ class ET(Inverter):
|
|
|
268
269
|
Current("meter_current1", 36055, "Meter L1 Current", Kind.GRID),
|
|
269
270
|
Current("meter_current2", 36056, "Meter L2 Current", Kind.GRID),
|
|
270
271
|
Current("meter_current3", 36057, "Meter L3 Current", Kind.GRID),
|
|
272
|
+
|
|
273
|
+
Energy8("meter_e_total_exp1", 36092, "Meter Total Energy (export) L1", Kind.GRID),
|
|
274
|
+
Energy8("meter_e_total_exp2", 36096, "Meter Total Energy (export) L2", Kind.GRID),
|
|
275
|
+
Energy8("meter_e_total_exp3", 36100, "Meter Total Energy (export) L3", Kind.GRID),
|
|
276
|
+
Energy8("meter_e_total_exp", 36104, "Meter Total Energy (export)", Kind.GRID),
|
|
277
|
+
Energy8("meter_e_total_imp1", 36108, "Meter Total Energy (import) L1", Kind.GRID),
|
|
278
|
+
Energy8("meter_e_total_imp2", 36112, "Meter Total Energy (import) L2", Kind.GRID),
|
|
279
|
+
Energy8("meter_e_total_imp3", 36116, "Meter Total Energy (import) L3", Kind.GRID),
|
|
280
|
+
Energy8("meter_e_total_imp", 36120, "Meter Total Energy (import)", Kind.GRID),
|
|
271
281
|
)
|
|
272
282
|
|
|
273
283
|
# Inverter's MPPT data
|
|
@@ -464,6 +474,7 @@ class ET(Inverter):
|
|
|
464
474
|
self._READ_RUNNING_DATA: ProtocolCommand = self._read_command(0x891c, 0x007d)
|
|
465
475
|
self._READ_METER_DATA: ProtocolCommand = self._read_command(0x8ca0, 0x2d)
|
|
466
476
|
self._READ_METER_DATA_EXTENDED: ProtocolCommand = self._read_command(0x8ca0, 0x3a)
|
|
477
|
+
self._READ_METER_DATA_EXTENDED2: ProtocolCommand = self._read_command(0x8ca0, 0x7d)
|
|
467
478
|
self._READ_BATTERY_INFO: ProtocolCommand = self._read_command(0x9088, 0x0018)
|
|
468
479
|
self._READ_BATTERY2_INFO: ProtocolCommand = self._read_command(0x9858, 0x0016)
|
|
469
480
|
self._READ_MPPT_DATA: ProtocolCommand = self._read_command(0x89e5, 0x3d)
|
|
@@ -472,6 +483,7 @@ class ET(Inverter):
|
|
|
472
483
|
self._has_battery: bool = True
|
|
473
484
|
self._has_battery2: bool = False
|
|
474
485
|
self._has_meter_extended: bool = False
|
|
486
|
+
self._has_meter_extended2: bool = False
|
|
475
487
|
self._has_mppt: bool = False
|
|
476
488
|
self._sensors = self.__all_sensors
|
|
477
489
|
self._sensors_battery = self.__all_sensors_battery
|
|
@@ -490,6 +502,11 @@ class ET(Inverter):
|
|
|
490
502
|
"""Filter to exclude extended meter sensors"""
|
|
491
503
|
return s.offset < 36045
|
|
492
504
|
|
|
505
|
+
@staticmethod
|
|
506
|
+
def _not_extended_meter2(s: Sensor) -> bool:
|
|
507
|
+
"""Filter to exclude extended meter sensors"""
|
|
508
|
+
return s.offset < 36058
|
|
509
|
+
|
|
493
510
|
async def read_device_info(self):
|
|
494
511
|
response = await self._read_from_socket(self._READ_DEVICE_VERSION_INFO)
|
|
495
512
|
response = response.response_data()
|
|
@@ -520,9 +537,10 @@ class ET(Inverter):
|
|
|
520
537
|
if is_2_battery(self) or self.rated_power >= 25000:
|
|
521
538
|
self._has_battery2 = True
|
|
522
539
|
|
|
523
|
-
if self.rated_power >= 15000:
|
|
540
|
+
if is_745_platform(self) or self.rated_power >= 15000:
|
|
524
541
|
self._has_mppt = True
|
|
525
542
|
self._has_meter_extended = True
|
|
543
|
+
self._has_meter_extended2 = True
|
|
526
544
|
else:
|
|
527
545
|
self._sensors_meter = tuple(filter(self._not_extended_meter, self._sensors_meter))
|
|
528
546
|
|
|
@@ -577,7 +595,21 @@ class ET(Inverter):
|
|
|
577
595
|
else:
|
|
578
596
|
raise ex
|
|
579
597
|
|
|
580
|
-
if self.
|
|
598
|
+
if self._has_meter_extended2:
|
|
599
|
+
try:
|
|
600
|
+
response = await self._read_from_socket(self._READ_METER_DATA_EXTENDED2)
|
|
601
|
+
data.update(self._map_response(response, self._sensors_meter))
|
|
602
|
+
except RequestRejectedException as ex:
|
|
603
|
+
if ex.message == ILLEGAL_DATA_ADDRESS:
|
|
604
|
+
logger.info("Extended meter values not supported, disabling further attempts.")
|
|
605
|
+
self._has_meter_extended2 = False
|
|
606
|
+
self._sensors_meter = tuple(filter(self._not_extended_meter2, self._sensors_meter))
|
|
607
|
+
response = await self._read_from_socket(self._READ_METER_DATA_EXTENDED)
|
|
608
|
+
data.update(
|
|
609
|
+
self._map_response(response, self._sensors_meter))
|
|
610
|
+
else:
|
|
611
|
+
raise ex
|
|
612
|
+
elif self._has_meter_extended:
|
|
581
613
|
try:
|
|
582
614
|
response = await self._read_from_socket(self._READ_METER_DATA_EXTENDED)
|
|
583
615
|
data.update(self._map_response(response, self._sensors_meter))
|
goodwe/model.py
CHANGED
|
@@ -48,3 +48,7 @@ def is_2_battery(inverter: Inverter) -> bool:
|
|
|
48
48
|
def is_745_platform(inverter: Inverter) -> bool:
|
|
49
49
|
return any(model in inverter.serial_number for model in PLATFORM_745_LV_MODELS) or any(
|
|
50
50
|
model in inverter.serial_number for model in PLATFORM_745_HV_MODELS)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def is_753_platform(inverter: Inverter) -> bool:
|
|
54
|
+
return any(model in inverter.serial_number for model in PLATFORM_753_MODELS)
|
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:
|
goodwe/sensor.py
CHANGED
|
@@ -197,6 +197,17 @@ class Energy4(Sensor):
|
|
|
197
197
|
return float(value) / 10 if value is not None else None
|
|
198
198
|
|
|
199
199
|
|
|
200
|
+
class Energy8(Sensor):
|
|
201
|
+
"""Sensor representing energy [kWh] value encoded in 8 bytes"""
|
|
202
|
+
|
|
203
|
+
def __init__(self, id_: str, offset: int, name: str, kind: Optional[SensorKind]):
|
|
204
|
+
super().__init__(id_, offset, name, 8, "kWh", kind)
|
|
205
|
+
|
|
206
|
+
def read_value(self, data: ProtocolResponse):
|
|
207
|
+
value = read_bytes8(data)
|
|
208
|
+
return float(value) / 100 if value is not None else None
|
|
209
|
+
|
|
210
|
+
|
|
200
211
|
class Apparent(Sensor):
|
|
201
212
|
"""Sensor representing apparent power [VA] value encoded in 2 bytes"""
|
|
202
213
|
|
|
@@ -840,6 +851,14 @@ def read_bytes4_signed(buffer: ProtocolResponse, offset: int = None) -> int:
|
|
|
840
851
|
return int.from_bytes(buffer.read(4), byteorder="big", signed=True)
|
|
841
852
|
|
|
842
853
|
|
|
854
|
+
def read_bytes8(buffer: ProtocolResponse, offset: int = None, undef: int = None) -> int:
|
|
855
|
+
"""Retrieve 8 byte (unsigned int) value from buffer"""
|
|
856
|
+
if offset is not None:
|
|
857
|
+
buffer.seek(offset)
|
|
858
|
+
value = int.from_bytes(buffer.read(8), byteorder="big", signed=False)
|
|
859
|
+
return undef if value == 0xffffffffffffffff else value
|
|
860
|
+
|
|
861
|
+
|
|
843
862
|
def read_decimal2(buffer: ProtocolResponse, scale: int, offset: int = None) -> float:
|
|
844
863
|
"""Retrieve 2 byte (signed float) value from buffer"""
|
|
845
864
|
if offset is not None:
|
|
@@ -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=IJxLDajuu2psYE5ZpwA2HFJDFdK5JIST6kUqWyRVBko,13663
|
|
4
|
+
goodwe/es.py,sha256=vvHmxcFykp8nhR1I8p7SF0YcYpvdCKBYacgcolbVHXI,23009
|
|
5
|
+
goodwe/et.py,sha256=Sdgqj13DXIg36NptkHMKxuP78oo4aUQ_6zlToyt78qI,46002
|
|
6
|
+
goodwe/exceptions.py,sha256=dKMLxotjoR1ic8OVlw1joIJ4mKWD6oFtUMZ86fNM5ZE,1403
|
|
7
|
+
goodwe/inverter.py,sha256=86aMJzJjNOr1I_tCF5H6mBwzDTjLbGDKUL2hbi0XSxg,10459
|
|
8
|
+
goodwe/modbus.py,sha256=Mg_s_v8kbZgqXZM6ZUUxkZx2boAG8LkuDG5OiFKK2X4,8402
|
|
9
|
+
goodwe/model.py,sha256=OAKfw6ggClgLR9JIdNd7tQ4pnh_7o_UqVdm1KOVsm-Y,2200
|
|
10
|
+
goodwe/protocol.py,sha256=2xRo1H53G6T0ANSuYKPK_KTNfCVTctIU2ZHVu-CvMPk,30163
|
|
11
|
+
goodwe/sensor.py,sha256=xeDZIwjJ_176ULrRXVCTYvVXx6o2_pWgS0KuR3PPQdg,38435
|
|
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,,
|
goodwe-0.4.6.dist-info/RECORD
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
goodwe/__init__.py,sha256=8fFGBBvBpCo6Ew4puTtW0kYo2hVPKUx6z5A-TA4Tbvc,5795
|
|
2
|
-
goodwe/const.py,sha256=yhWk56YV7k7-MbgfmWEMYNlqeRNLOfOpfTqEfRj6Hp8,7934
|
|
3
|
-
goodwe/dt.py,sha256=8lbc-G1vQUxtYZRDAHi6QW04bFJRW7l4OjBVDp0EjZc,12093
|
|
4
|
-
goodwe/es.py,sha256=vvHmxcFykp8nhR1I8p7SF0YcYpvdCKBYacgcolbVHXI,23009
|
|
5
|
-
goodwe/et.py,sha256=f4XWaV8Ltywlca1GYJCUM46ziQCtKleser34rO09WQg,44053
|
|
6
|
-
goodwe/exceptions.py,sha256=dKMLxotjoR1ic8OVlw1joIJ4mKWD6oFtUMZ86fNM5ZE,1403
|
|
7
|
-
goodwe/inverter.py,sha256=86aMJzJjNOr1I_tCF5H6mBwzDTjLbGDKUL2hbi0XSxg,10459
|
|
8
|
-
goodwe/modbus.py,sha256=Mg_s_v8kbZgqXZM6ZUUxkZx2boAG8LkuDG5OiFKK2X4,8402
|
|
9
|
-
goodwe/model.py,sha256=dWBjMFJMnhZoUdDd9fGT54DERDANz4TirK0Wy8kWMbk,2068
|
|
10
|
-
goodwe/protocol.py,sha256=gnQ1vV4U_lPpaNq5-jmzJO6ngJEDFVo0jWXVujSyu_0,30083
|
|
11
|
-
goodwe/sensor.py,sha256=gns0508zi3dCQ4C01RIUQ8aH-2b8TDpCjW71CFEVEiY,37707
|
|
12
|
-
goodwe-0.4.6.dist-info/LICENSE,sha256=aZAhk3lRdYT1YZV-IKRHISEcc_KNUmgfuNO3QhRamNM,1073
|
|
13
|
-
goodwe-0.4.6.dist-info/METADATA,sha256=jKbTn5rYaHEb6B8TpTYNqhE1F9dDApMFpBiwvJjMoPQ,3376
|
|
14
|
-
goodwe-0.4.6.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
15
|
-
goodwe-0.4.6.dist-info/top_level.txt,sha256=kKoiqiVvAxDaDJYMZZQLgHQj9cuWT1MXLfXElTDuf8s,7
|
|
16
|
-
goodwe-0.4.6.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|