goodwe 0.4.0__tar.gz → 0.4.2__tar.gz
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-0.4.0/goodwe.egg-info → goodwe-0.4.2}/PKG-INFO +1 -1
- goodwe-0.4.2/VERSION +1 -0
- {goodwe-0.4.0 → goodwe-0.4.2}/goodwe/dt.py +40 -13
- {goodwe-0.4.0 → goodwe-0.4.2}/goodwe/es.py +7 -6
- {goodwe-0.4.0 → goodwe-0.4.2}/goodwe/et.py +103 -38
- {goodwe-0.4.0 → goodwe-0.4.2}/goodwe/exceptions.py +14 -0
- {goodwe-0.4.0 → goodwe-0.4.2}/goodwe/inverter.py +13 -9
- {goodwe-0.4.0 → goodwe-0.4.2}/goodwe/modbus.py +10 -9
- {goodwe-0.4.0 → goodwe-0.4.2}/goodwe/protocol.py +115 -49
- {goodwe-0.4.0 → goodwe-0.4.2}/goodwe/sensor.py +28 -4
- {goodwe-0.4.0 → goodwe-0.4.2/goodwe.egg-info}/PKG-INFO +1 -1
- {goodwe-0.4.0 → goodwe-0.4.2}/tests/test_dt.py +1 -1
- {goodwe-0.4.0 → goodwe-0.4.2}/tests/test_es.py +15 -1
- {goodwe-0.4.0 → goodwe-0.4.2}/tests/test_et.py +158 -25
- {goodwe-0.4.0 → goodwe-0.4.2}/tests/test_modbus.py +12 -4
- {goodwe-0.4.0 → goodwe-0.4.2}/tests/test_protocol.py +1 -1
- {goodwe-0.4.0 → goodwe-0.4.2}/tests/test_sensor.py +11 -0
- goodwe-0.4.0/VERSION +0 -1
- {goodwe-0.4.0 → goodwe-0.4.2}/LICENSE +0 -0
- {goodwe-0.4.0 → goodwe-0.4.2}/README.md +0 -0
- {goodwe-0.4.0 → goodwe-0.4.2}/goodwe/__init__.py +0 -0
- {goodwe-0.4.0 → goodwe-0.4.2}/goodwe/const.py +0 -0
- {goodwe-0.4.0 → goodwe-0.4.2}/goodwe/model.py +0 -0
- {goodwe-0.4.0 → goodwe-0.4.2}/goodwe.egg-info/SOURCES.txt +0 -0
- {goodwe-0.4.0 → goodwe-0.4.2}/goodwe.egg-info/dependency_links.txt +0 -0
- {goodwe-0.4.0 → goodwe-0.4.2}/goodwe.egg-info/top_level.txt +0 -0
- {goodwe-0.4.0 → goodwe-0.4.2}/pyproject.toml +0 -0
- {goodwe-0.4.0 → goodwe-0.4.2}/setup.cfg +0 -0
goodwe-0.4.2/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.4.2
|
|
@@ -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))
|
|
@@ -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:
|
|
@@ -3,10 +3,11 @@ from __future__ import annotations
|
|
|
3
3
|
import logging
|
|
4
4
|
from typing import Tuple
|
|
5
5
|
|
|
6
|
-
from .exceptions import RequestRejectedException
|
|
6
|
+
from .exceptions import RequestFailedException, RequestRejectedException
|
|
7
7
|
from .inverter import Inverter
|
|
8
8
|
from .inverter import OperationMode
|
|
9
9
|
from .inverter import SensorKind as Kind
|
|
10
|
+
from .modbus import ILLEGAL_DATA_ADDRESS
|
|
10
11
|
from .model import is_2_battery, is_4_mppt, is_745_platform, is_single_phase
|
|
11
12
|
from .protocol import ProtocolCommand
|
|
12
13
|
from .sensor import *
|
|
@@ -331,7 +332,7 @@ class ET(Inverter):
|
|
|
331
332
|
# Modbus registers of inverter settings, offsets are modbus register addresses
|
|
332
333
|
__all_settings: Tuple[Sensor, ...] = (
|
|
333
334
|
Integer("comm_address", 45127, "Communication Address", ""),
|
|
334
|
-
|
|
335
|
+
Integer("modbus_baud_rate", 45132, "Modbus Baud rate", ""),
|
|
335
336
|
Timestamp("time", 45200, "Inverter time"),
|
|
336
337
|
|
|
337
338
|
Integer("sensitivity_check", 45246, "Sensitivity Check Mode", "", Kind.AC),
|
|
@@ -371,6 +372,51 @@ class ET(Inverter):
|
|
|
371
372
|
ByteH("eco_mode_3_switch", 47526, "Eco Mode Group 3 Switch"),
|
|
372
373
|
EcoModeV1("eco_mode_4", 47527, "Eco Mode Group 4"),
|
|
373
374
|
ByteH("eco_mode_4_switch", 47530, "Eco Mode Group 4 Switch"),
|
|
375
|
+
|
|
376
|
+
# Direct BMS communication for EMS Control
|
|
377
|
+
Integer("bms_version", 47900, "BMS Version"),
|
|
378
|
+
Integer("bms_bat_modules", 47901, "BMS Battery Modules"),
|
|
379
|
+
# Real time read from BMS
|
|
380
|
+
Voltage("bms_bat_charge_v_max", 47902, "BMS Battery Charge Voltage (max)", Kind.BMS),
|
|
381
|
+
Current("bms_bat_charge_i_max", 47903, "BMS Battery Charge Current (max)", Kind.BMS),
|
|
382
|
+
Voltage("bms_bat_discharge_v_min", 47904, "BMS min. Battery Discharge Voltage (min)", Kind.BMS),
|
|
383
|
+
Current("bms_bat_discharge_i_max", 47905, "BMS max. Battery Discharge Current (max)", Kind.BMS),
|
|
384
|
+
Voltage("bms_bat_voltage", 47906, "BMS Battery Voltage", Kind.BMS),
|
|
385
|
+
Current("bms_bat_current", 47907, "BMS Battery Current", Kind.BMS),
|
|
386
|
+
#
|
|
387
|
+
Integer("bms_bat_soc", 47908, "BMS Battery State of Charge", "%", Kind.BMS),
|
|
388
|
+
Integer("bms_bat_soh", 47909, "BMS Battery State of Health", "%", Kind.BMS),
|
|
389
|
+
Temp("bms_bat_temperature", 47910, "BMS Battery Temperature", Kind.BMS),
|
|
390
|
+
Long("bms_bat_warning-code", 47911, "BMS Battery Warning Code"),
|
|
391
|
+
# Reserved
|
|
392
|
+
Long("bms_bat_alarm-code", 47913, "BMS Battery Alarm Code"),
|
|
393
|
+
Integer("bms_status", 47915, "BMS Status"),
|
|
394
|
+
Integer("bms_comm_loss_disable", 47916, "BMS Communication Loss Disable"),
|
|
395
|
+
# RW settings of BMS voltage rate
|
|
396
|
+
Integer("bms_battery_string_rate_v", 47917, "BMS Battery String Rate Voltage"),
|
|
397
|
+
|
|
398
|
+
# Direct BMS communication for EMS Control
|
|
399
|
+
Integer("bms2_version", 47918, "BMS2 Version"),
|
|
400
|
+
Integer("bms2_bat_modules", 47919, "BMS2 Battery Modules"),
|
|
401
|
+
# Real time read from BMS
|
|
402
|
+
Voltage("bms2_bat_charge_v_max", 47920, "BMS2 Battery Charge Voltage (max)", Kind.BMS),
|
|
403
|
+
Current("bms2_bat_charge_i_max", 47921, "BMS2 Battery Charge Current (max)", Kind.BMS),
|
|
404
|
+
Voltage("bms2_bat_discharge_v_min", 47922, "BMS2 min. Battery Discharge Voltage (min)", Kind.BMS),
|
|
405
|
+
Current("bms2_bat_discharge_i_max", 47923, "BMS2 max. Battery Discharge Current (max)", Kind.BMS),
|
|
406
|
+
Voltage("bms2_bat_voltage", 47924, "BMS2 Battery Voltage", Kind.BMS),
|
|
407
|
+
Current("bms2_bat_current", 47925, "BMS2 Battery Current", Kind.BMS),
|
|
408
|
+
#
|
|
409
|
+
Integer("bms2_bat_soc", 47926, "BMS2 Battery State of Charge", "%", Kind.BMS),
|
|
410
|
+
Integer("bms2_bat_soh", 47927, "BMS2 Battery State of Health", "%", Kind.BMS),
|
|
411
|
+
Temp("bms2_bat_temperature", 47928, "BMS2 Battery Temperature", Kind.BMS),
|
|
412
|
+
Long("bms2_bat_warning-code", 47929, "BMS2 Battery Warning Code"),
|
|
413
|
+
# Reserved
|
|
414
|
+
Long("bms2_bat_alarm-code", 47931, "BMS2 Battery Alarm Code"),
|
|
415
|
+
Integer("bms2_status", 47933, "BMS2 Status"),
|
|
416
|
+
Integer("bms2_comm_loss_disable", 47934, "BMS2 Communication Loss Disable"),
|
|
417
|
+
# RW settings of BMS voltage rate
|
|
418
|
+
Integer("bms2_battery_string_rate_v", 47935, "BMS2 Battery String Rate Voltage"),
|
|
419
|
+
|
|
374
420
|
)
|
|
375
421
|
|
|
376
422
|
# Settings added in ARM firmware 19
|
|
@@ -389,6 +435,7 @@ class ET(Inverter):
|
|
|
389
435
|
Integer("load_control_mode", 47595, "Load Control Mode", "", Kind.AC),
|
|
390
436
|
Integer("load_control_switch", 47596, "Load Control Switch", "", Kind.AC),
|
|
391
437
|
Integer("load_control_soc", 47597, "Load Control SoC", "", Kind.AC),
|
|
438
|
+
Integer("hardware_feed_power", 47599, "Hardware Feed Power"),
|
|
392
439
|
|
|
393
440
|
Integer("fast_charging_power", 47603, "Fast Charging Power", "%", Kind.BAT),
|
|
394
441
|
)
|
|
@@ -410,10 +457,7 @@ class ET(Inverter):
|
|
|
410
457
|
)
|
|
411
458
|
|
|
412
459
|
def __init__(self, host: str, port: int, comm_addr: int = 0, timeout: int = 1, retries: int = 3):
|
|
413
|
-
super().__init__(host, port, comm_addr, timeout, retries)
|
|
414
|
-
if not self.comm_addr:
|
|
415
|
-
# Set the default inverter address
|
|
416
|
-
self.comm_addr = 0xf7
|
|
460
|
+
super().__init__(host, port, comm_addr if comm_addr else 0xf7, timeout, retries)
|
|
417
461
|
self._READ_DEVICE_VERSION_INFO: ProtocolCommand = self._read_command(0x88b8, 0x0021)
|
|
418
462
|
self._READ_RUNNING_DATA: ProtocolCommand = self._read_command(0x891c, 0x007d)
|
|
419
463
|
self._READ_METER_DATA: ProtocolCommand = self._read_command(0x8ca0, 0x2d)
|
|
@@ -447,19 +491,19 @@ class ET(Inverter):
|
|
|
447
491
|
async def read_device_info(self):
|
|
448
492
|
response = await self._read_from_socket(self._READ_DEVICE_VERSION_INFO)
|
|
449
493
|
response = response.response_data()
|
|
450
|
-
# Modbus registers from
|
|
494
|
+
# Modbus registers from 35000 - 35032
|
|
451
495
|
self.modbus_version = read_unsigned_int(response, 0)
|
|
452
496
|
self.rated_power = read_unsigned_int(response, 2)
|
|
453
497
|
self.ac_output_type = read_unsigned_int(response, 4) # 0: 1-phase, 1: 3-phase (4 wire), 2: 3-phase (3 wire)
|
|
454
|
-
self.serial_number = self._decode(response[6:22])
|
|
455
|
-
self.model_name = self._decode(response[22:32])
|
|
456
|
-
self.dsp1_version = read_unsigned_int(response, 32)
|
|
457
|
-
self.dsp2_version = read_unsigned_int(response, 34)
|
|
458
|
-
self.dsp_svn_version = read_unsigned_int(response, 36)
|
|
459
|
-
self.arm_version = read_unsigned_int(response, 38)
|
|
460
|
-
self.arm_svn_version = read_unsigned_int(response, 40)
|
|
461
|
-
self.firmware = self._decode(response[42:54])
|
|
462
|
-
self.arm_firmware = self._decode(response[54:66])
|
|
498
|
+
self.serial_number = self._decode(response[6:22]) # 35003 - 350010
|
|
499
|
+
self.model_name = self._decode(response[22:32]) # 35011 - 35015
|
|
500
|
+
self.dsp1_version = read_unsigned_int(response, 32) # 35016
|
|
501
|
+
self.dsp2_version = read_unsigned_int(response, 34) # 35017
|
|
502
|
+
self.dsp_svn_version = read_unsigned_int(response, 36) # 35018
|
|
503
|
+
self.arm_version = read_unsigned_int(response, 38) # 35019
|
|
504
|
+
self.arm_svn_version = read_unsigned_int(response, 40) # 35020
|
|
505
|
+
self.firmware = self._decode(response[42:54]) # 35021 - 35027
|
|
506
|
+
self.arm_firmware = self._decode(response[54:66]) # 35027 - 35032
|
|
463
507
|
|
|
464
508
|
if not is_4_mppt(self) and self.rated_power < 15000:
|
|
465
509
|
# This inverter does not have 4 MPPTs or PV strings
|
|
@@ -485,18 +529,24 @@ class ET(Inverter):
|
|
|
485
529
|
await self._read_from_socket(self._read_command(47547, 6))
|
|
486
530
|
self._settings.update({s.id_: s for s in self.__settings_arm_fw_19})
|
|
487
531
|
except RequestRejectedException as ex:
|
|
488
|
-
if ex.message ==
|
|
489
|
-
logger.debug("
|
|
532
|
+
if ex.message == ILLEGAL_DATA_ADDRESS:
|
|
533
|
+
logger.debug("EcoModeV2 settings not supported, switching to EcoModeV1.")
|
|
490
534
|
self._has_eco_mode_v2 = False
|
|
535
|
+
except RequestFailedException:
|
|
536
|
+
logger.debug("Cannot read EcoModeV2 settings, switching to EcoModeV1.")
|
|
537
|
+
self._has_eco_mode_v2 = False
|
|
491
538
|
|
|
492
539
|
# Check and add Peak Shaving settings added in (ETU fw 22)
|
|
493
540
|
try:
|
|
494
541
|
await self._read_from_socket(self._read_command(47589, 6))
|
|
495
542
|
self._settings.update({s.id_: s for s in self.__settings_arm_fw_22})
|
|
496
543
|
except RequestRejectedException as ex:
|
|
497
|
-
if ex.message ==
|
|
498
|
-
logger.debug("
|
|
544
|
+
if ex.message == ILLEGAL_DATA_ADDRESS:
|
|
545
|
+
logger.debug("PeakShaving setting not supported, disabling it.")
|
|
499
546
|
self._has_peak_shaving = False
|
|
547
|
+
except RequestFailedException:
|
|
548
|
+
logger.debug("Cannot read _has_peak_shaving settings, disabling it.")
|
|
549
|
+
self._has_peak_shaving = False
|
|
500
550
|
|
|
501
551
|
async def read_runtime_data(self) -> Dict[str, Any]:
|
|
502
552
|
response = await self._read_from_socket(self._READ_RUNNING_DATA)
|
|
@@ -508,8 +558,8 @@ class ET(Inverter):
|
|
|
508
558
|
response = await self._read_from_socket(self._READ_BATTERY_INFO)
|
|
509
559
|
data.update(self._map_response(response, self._sensors_battery))
|
|
510
560
|
except RequestRejectedException as ex:
|
|
511
|
-
if ex.message ==
|
|
512
|
-
logger.
|
|
561
|
+
if ex.message == ILLEGAL_DATA_ADDRESS:
|
|
562
|
+
logger.info("Battery values not supported, disabling further attempts.")
|
|
513
563
|
self._has_battery = False
|
|
514
564
|
else:
|
|
515
565
|
raise ex
|
|
@@ -519,8 +569,8 @@ class ET(Inverter):
|
|
|
519
569
|
data.update(
|
|
520
570
|
self._map_response(response, self._sensors_battery2))
|
|
521
571
|
except RequestRejectedException as ex:
|
|
522
|
-
if ex.message ==
|
|
523
|
-
logger.
|
|
572
|
+
if ex.message == ILLEGAL_DATA_ADDRESS:
|
|
573
|
+
logger.info("Battery 2 values not supported, disabling further attempts.")
|
|
524
574
|
self._has_battery2 = False
|
|
525
575
|
else:
|
|
526
576
|
raise ex
|
|
@@ -530,8 +580,8 @@ class ET(Inverter):
|
|
|
530
580
|
response = await self._read_from_socket(self._READ_METER_DATA_EXTENDED)
|
|
531
581
|
data.update(self._map_response(response, self._sensors_meter))
|
|
532
582
|
except RequestRejectedException as ex:
|
|
533
|
-
if ex.message ==
|
|
534
|
-
logger.
|
|
583
|
+
if ex.message == ILLEGAL_DATA_ADDRESS:
|
|
584
|
+
logger.info("Extended meter values not supported, disabling further attempts.")
|
|
535
585
|
self._has_meter_extended = False
|
|
536
586
|
self._sensors_meter = tuple(filter(self._not_extended_meter, self._sensors_meter))
|
|
537
587
|
response = await self._read_from_socket(self._READ_METER_DATA)
|
|
@@ -548,8 +598,8 @@ class ET(Inverter):
|
|
|
548
598
|
response = await self._read_from_socket(self._READ_MPPT_DATA)
|
|
549
599
|
data.update(self._map_response(response, self._sensors_mppt))
|
|
550
600
|
except RequestRejectedException as ex:
|
|
551
|
-
if ex.message ==
|
|
552
|
-
logger.
|
|
601
|
+
if ex.message == ILLEGAL_DATA_ADDRESS:
|
|
602
|
+
logger.info("MPPT values not supported, disabling further attempts.")
|
|
553
603
|
self._has_mppt = False
|
|
554
604
|
else:
|
|
555
605
|
raise ex
|
|
@@ -558,20 +608,35 @@ class ET(Inverter):
|
|
|
558
608
|
|
|
559
609
|
async def read_setting(self, setting_id: str) -> Any:
|
|
560
610
|
setting = self._settings.get(setting_id)
|
|
561
|
-
if
|
|
562
|
-
|
|
563
|
-
|
|
611
|
+
if setting:
|
|
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}"')
|
|
564
619
|
|
|
565
620
|
async def _read_setting(self, setting: Sensor) -> Any:
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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)
|
|
625
|
+
except RequestRejectedException as ex:
|
|
626
|
+
if ex.message == ILLEGAL_DATA_ADDRESS:
|
|
627
|
+
logger.debug("Unsupported setting %s", setting.id_)
|
|
628
|
+
self._settings.pop(setting.id_, None)
|
|
629
|
+
return None
|
|
569
630
|
|
|
570
631
|
async def write_setting(self, setting_id: str, value: Any):
|
|
571
632
|
setting = self._settings.get(setting_id)
|
|
572
|
-
if
|
|
573
|
-
|
|
574
|
-
|
|
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}"')
|
|
575
640
|
|
|
576
641
|
async def _write_setting(self, setting: Sensor, value: Any):
|
|
577
642
|
if setting.size_ == 1:
|
|
@@ -592,7 +657,7 @@ class ET(Inverter):
|
|
|
592
657
|
try:
|
|
593
658
|
value = await self.read_setting(setting.id_)
|
|
594
659
|
data[setting.id_] = value
|
|
595
|
-
except ValueError:
|
|
660
|
+
except (ValueError, RequestFailedException):
|
|
596
661
|
logger.exception("Error reading setting %s.", setting.id_)
|
|
597
662
|
data[setting.id_] = None
|
|
598
663
|
return data
|
|
@@ -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"""
|
|
@@ -22,6 +22,7 @@ class SensorKind(Enum):
|
|
|
22
22
|
UPS - inverter ups/eps/backup output (e.g. ac voltage of backup/off-grid connected output)
|
|
23
23
|
BAT - battery (e.g. dc voltage of connected battery pack)
|
|
24
24
|
GRID - power grid/smart meter (e.g. active power exported to grid)
|
|
25
|
+
BMS - BMS direct data (e.g. dc voltage of)
|
|
25
26
|
"""
|
|
26
27
|
|
|
27
28
|
PV = 1
|
|
@@ -29,6 +30,7 @@ class SensorKind(Enum):
|
|
|
29
30
|
UPS = 3
|
|
30
31
|
BAT = 4
|
|
31
32
|
GRID = 5
|
|
33
|
+
BMS = 6
|
|
32
34
|
|
|
33
35
|
|
|
34
36
|
@dataclass
|
|
@@ -87,10 +89,9 @@ class Inverter(ABC):
|
|
|
87
89
|
"""
|
|
88
90
|
|
|
89
91
|
def __init__(self, host: str, port: int, comm_addr: int = 0, timeout: int = 1, retries: int = 3):
|
|
90
|
-
self._protocol: InverterProtocol = self._create_protocol(host, port, timeout, retries)
|
|
92
|
+
self._protocol: InverterProtocol = self._create_protocol(host, port, comm_addr, timeout, retries)
|
|
91
93
|
self._consecutive_failures_count: int = 0
|
|
92
|
-
|
|
93
|
-
self.comm_addr: int = comm_addr
|
|
94
|
+
self.keep_alive: bool = True
|
|
94
95
|
|
|
95
96
|
self.model_name: str | None = None
|
|
96
97
|
self.serial_number: str | None = None
|
|
@@ -107,15 +108,15 @@ class Inverter(ABC):
|
|
|
107
108
|
|
|
108
109
|
def _read_command(self, offset: int, count: int) -> ProtocolCommand:
|
|
109
110
|
"""Create read protocol command."""
|
|
110
|
-
return self._protocol.read_command(
|
|
111
|
+
return self._protocol.read_command(offset, count)
|
|
111
112
|
|
|
112
113
|
def _write_command(self, register: int, value: int) -> ProtocolCommand:
|
|
113
114
|
"""Create write protocol command."""
|
|
114
|
-
return self._protocol.write_command(
|
|
115
|
+
return self._protocol.write_command(register, value)
|
|
115
116
|
|
|
116
117
|
def _write_multi_command(self, offset: int, values: bytes) -> ProtocolCommand:
|
|
117
118
|
"""Create write multiple protocol command."""
|
|
118
|
-
return self._protocol.write_multi_command(
|
|
119
|
+
return self._protocol.write_multi_command(offset, values)
|
|
119
120
|
|
|
120
121
|
async def _read_from_socket(self, command: ProtocolCommand) -> ProtocolResponse:
|
|
121
122
|
try:
|
|
@@ -129,6 +130,9 @@ class Inverter(ABC):
|
|
|
129
130
|
except RequestFailedException as ex:
|
|
130
131
|
self._consecutive_failures_count += 1
|
|
131
132
|
raise RequestFailedException(ex.message, self._consecutive_failures_count) from None
|
|
133
|
+
finally:
|
|
134
|
+
if not self.keep_alive:
|
|
135
|
+
self._protocol.close_transport()
|
|
132
136
|
|
|
133
137
|
@abstractmethod
|
|
134
138
|
async def read_device_info(self):
|
|
@@ -268,11 +272,11 @@ class Inverter(ABC):
|
|
|
268
272
|
raise NotImplementedError()
|
|
269
273
|
|
|
270
274
|
@staticmethod
|
|
271
|
-
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:
|
|
272
276
|
if port == 502:
|
|
273
|
-
return TcpInverterProtocol(host, port, timeout, retries)
|
|
277
|
+
return TcpInverterProtocol(host, port, comm_addr, timeout, retries)
|
|
274
278
|
else:
|
|
275
|
-
return UdpInverterProtocol(host, port, timeout, retries)
|
|
279
|
+
return UdpInverterProtocol(host, port, comm_addr, timeout, retries)
|
|
276
280
|
|
|
277
281
|
@staticmethod
|
|
278
282
|
def _map_response(response: ProtocolResponse, sensors: Tuple[Sensor, ...]) -> Dict[str, Any]:
|
|
@@ -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,9 +9,11 @@ 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: str = 'ILLEGAL DATA ADDRESS'
|
|
13
|
+
|
|
12
14
|
FAILURE_CODES = {
|
|
13
15
|
1: "ILLEGAL FUNCTION",
|
|
14
|
-
2:
|
|
16
|
+
2: ILLEGAL_DATA_ADDRESS,
|
|
15
17
|
3: "ILLEGAL DATA VALUE",
|
|
16
18
|
4: "SLAVE DEVICE FAILURE",
|
|
17
19
|
5: "ACKNOWLEDGE",
|
|
@@ -176,8 +178,7 @@ def validate_modbus_rtu_response(data: bytes, cmd: int, offset: int, value: int)
|
|
|
176
178
|
return False
|
|
177
179
|
expected_length = data[4] + 7
|
|
178
180
|
if len(data) < expected_length:
|
|
179
|
-
|
|
180
|
-
return False
|
|
181
|
+
raise PartialResponseException(len(data), expected_length)
|
|
181
182
|
elif data[3] in (MODBUS_WRITE_CMD, MODBUS_WRITE_MULTI_CMD):
|
|
182
183
|
if len(data) < 10:
|
|
183
184
|
logger.debug("Response has unexpected length: %d, expected %d.", len(data), 10)
|
|
@@ -220,18 +221,18 @@ def validate_modbus_tcp_response(data: bytes, cmd: int, offset: int, value: int)
|
|
|
220
221
|
if len(data) <= 8:
|
|
221
222
|
logger.debug("Response is too short.")
|
|
222
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
|
+
|
|
223
228
|
if data[7] == MODBUS_READ_CMD:
|
|
224
229
|
if data[8] != value * 2:
|
|
225
230
|
logger.debug("Response has unexpected length: %d, expected %d.", data[8], value * 2)
|
|
226
231
|
return False
|
|
227
|
-
expected_length = data[8] + 9
|
|
228
|
-
if len(data) < expected_length:
|
|
229
|
-
logger.debug("Response is too short: %d, expected %d.", len(data), expected_length)
|
|
230
|
-
return False
|
|
231
232
|
elif data[7] in (MODBUS_WRITE_CMD, MODBUS_WRITE_MULTI_CMD):
|
|
232
233
|
if len(data) < 12:
|
|
233
234
|
logger.debug("Response has unexpected length: %d, expected %d.", len(data), 14)
|
|
234
|
-
|
|
235
|
+
raise PartialResponseException(len(data), expected_length)
|
|
235
236
|
response_offset = int.from_bytes(data[8:10], byteorder='big', signed=False)
|
|
236
237
|
if response_offset != offset:
|
|
237
238
|
logger.debug("Response has wrong offset: %X, expected %X.", response_offset, offset)
|