Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updated DT Series data from GoodWe documentation (shared to Discord channel) #89

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 72 additions & 43 deletions goodwe/dt.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,21 +67,19 @@ class DT(Inverter):
Calculated("pgrid3",
lambda data: round(read_voltage(data, 30120) * read_current(data, 30123)),
"On-grid L3 Power", "W", Kind.AC),
# 30127 reserved
PowerS("total_inverter_power", 30128, "Total Power", Kind.AC),
Power4("total_inverter_power", 30127, "Total Power", Kind.AC),
Integer("work_mode", 30129, "Work Mode code"),
Enum2("work_mode_label", 30129, WORK_MODES, "Work Mode"),
Long("error_codes", 30130, "Error Codes"),
Integer("warning_code", 30132, "Warning code"),
Apparent4("apparent_power", 30133, "Apparent Power", Kind.AC),
Reactive4("reactive_power", 30135, "Reactive Power", Kind.AC),
# 30137 reserved
# 30138 reserved
PowerS("total_input_power", 30138, "Total Input Power", Kind.PV),
Decimal("power_factor", 30139, 1000, "Power Factor", "", Kind.GRID),
# 30140 reserved
# 30140 inverter efficiency
Temp("temperature", 30141, "Inverter Temperature", Kind.AC),
# 30142 reserved
# 30143 reserved
Temp("temperature_heatsink", 30142, "Heatsink Temperature", Kind.AC),
# Temp("temperature_module", 30143, "Module Temperature", Kind.AC),
Energy("e_day", 30144, "Today's PV Generation", Kind.PV),
Energy4("e_total", 30145, "Total PV Generation", Kind.PV),
Long("h_total", 30147, "Hours Total", "h", Kind.PV),
Expand All @@ -99,7 +97,7 @@ class DT(Inverter):
# 30159 reserved
# 30160 reserved
# 30161 reserved
Integer("funbit", 30162, "FunBit", "", Kind.PV),
Integer("functionbit", 30162, "FunctionBit", "", Kind.PV),
Voltage("vbus", 30163, "Bus Voltage", Kind.PV),
Voltage("vnbus", 30164, "NBus Voltage", Kind.PV),
Long("derating_mode", 30165, "Derating Mode code"),
Expand All @@ -109,20 +107,30 @@ class DT(Inverter):
# 30169 reserved
# 30170 reserved
# 30171 reserved
# 30172 reserved
Integer("rssi", 30172, "RSSI"),
)

# Inverter's meter data
# Modbus registers from offset 0x75f4 (30196)
# Modbus registers from offset 0x75f3 (30195)
__all_sensors_meter: tuple[Sensor, ...] = (
PowerS("active_power", 30196, "Active Power", Kind.GRID),
Power4S("meter_active_power", 30195, "Meter Active Power", Kind.GRID),
Energy4W("meter_e_total_exp", 30197, "Meter Total Energy (export)", Kind.GRID),
Energy4W("meter_e_total_imp", 30199, "Meter Total Energy (import)", Kind.GRID),
Integer("meter_comm_status", 30209, "Meter Communication Status"), # 1 OK, 0 NotOK



)

# Modbus registers of inverter settings, offsets are modbus register addresses
__all_settings: tuple[Sensor, ...] = (
Timestamp("time", 40313, "Inverter time"),

Integer("shadow_scan", 40326, "Shadow Scan", "", Kind.PV),
Integer("shadow_scan_pv1", 40326, "Shadow Scan Status PV1", "", Kind.PV),
Integer("shadow_scan_pv2", 40352, "Shadow Scan Status PV2", "", Kind.PV),
Integer("shadow_scan_pv3", 40362, "Shadow Scan Status PV3", "", Kind.PV),
Integer("shadow_scan_pv1_time", 40347, "Shadow Scan PV1 Time", "", Kind.PV),
Integer("shadow_scan_pv2_time", 40353, "Shadow Scan PV2 Time", "", Kind.PV),
# Integer("shadow_scan_pv3_time", 40xxx, "Shadow Scan PV3 Time", "", Kind.PV), #TBC
Integer("grid_export", 40327, "Grid Export Limit Enabled", "", Kind.GRID),
Integer("grid_export_limit", 40328, "Grid Export Limit", "%", Kind.GRID),
Integer("start", 40330, "Start / Power On", "", Kind.GRID),
Expand All @@ -142,10 +150,11 @@ class DT(Inverter):
)

def __init__(self, host: str, port: int, comm_addr: int = 0, timeout: int = 1, retries: int = 3):
super().__init__(host, port, comm_addr if comm_addr else 0x7f, timeout, retries)
self._READ_DEVICE_VERSION_INFO: ProtocolCommand = self._read_command(0x7531, 0x0028)
super().__init__(host, port, comm_addr if comm_addr else 0xf7, timeout, retries)
self._READ_DEVICE_VERSION_INFO: ProtocolCommand = self._read_command(0x7531, 0x0063)
self._READ_DEVICE_MODEL: ProtocolCommand = self._read_command(0x9CED, 0x0008)
self._READ_RUNNING_DATA: ProtocolCommand = self._read_command(0x7594, 0x0049)
self._READ_METER_DATA: ProtocolCommand = self._read_command(0x75f4, 0x01)
self._READ_METER_DATA: ProtocolCommand = self._read_command(0x75f3, 0xF)
self._sensors = self.__all_sensors
self._sensors_meter = self.__all_sensors_meter
self._settings: dict[str, Sensor] = {s.id_: s for s in self.__all_settings}
Expand All @@ -162,35 +171,55 @@ def _pv1_pv2_only(s: Sensor) -> bool:
"""Filter to exclude sensors on < 3 PV inverters"""
return not s.id_.endswith('pv3')

async def read_device_info(self):
response = await self._read_from_socket(self._READ_DEVICE_VERSION_INFO)
response = response.response_data()
async def read_device_info(self) -> dict[str, Any]:
device_info = {}

try:
self.model_name = response[22:32].decode("ascii").rstrip()
except:
print("No model name sent from the inverter.")
# Modbus registers from 30001 - 30040
self.serial_number = self._decode(response[6:22]) # 30004 - 30012
self.dsp1_version = read_unsigned_int(response, 66) # 30034
self.dsp2_version = read_unsigned_int(response, 68) # 30035
self.arm_version = read_unsigned_int(response, 70) # 30036
self.dsp_svn_version = read_unsigned_int(response, 72) # 35037
self.arm_svn_version = read_unsigned_int(response, 74) # 35038
self.firmware = f"{self.dsp1_version}.{self.dsp2_version}.{self.arm_version:02x}"

if is_single_phase(self):
# this is single phase inverter, filter out all L2 and L3 sensors
self._sensors = tuple(filter(self._single_phase_only, self.__all_sensors))
self._settings.update({s.id_: s for s in self.__settings_single_phase})
else:
self._settings.update({s.id_: s for s in self.__settings_three_phase})
response = await self._read_from_socket(self._READ_DEVICE_VERSION_INFO)
response = response.response_data()
self.model_name = response[22:32].decode("ascii").rstrip('\x00').strip()
except Exception as e:
try:
response2 = await self._read_from_socket(self._READ_DEVICE_MODEL)
response2 = response2.response_data()
self.model_name = response2[0:16].decode("ascii").rstrip('\x00').strip()
except Exception as e:
print(f"No model name sent from the inverter. Error: {e}")
self.model_name = "Unknown"

try:
# Modbus registers from 30001 - 30099
self.serial_number = self._decode(response[6:22]) # 30004 - 30012
self.meter_serial_number = self._decode(response[148:162]) #30075 - 30082
self.dsp1_version = read_unsigned_int(response, 66) # 30034
self.dsp2_version = read_unsigned_int(response, 68) # 30035
self.arm_version = read_unsigned_int(response, 70) # 30036
self.dsp_svn_version = read_unsigned_int(response, 72) # 35037
self.arm_svn_version = read_unsigned_int(response, 74) # 35038
self.meter_software_version = read_unsigned_int(response, 124) #30063
self.firmware = f"{self.dsp1_version}.{self.dsp2_version}.{self.arm_version:02x}"

device_info['model_name'] = self.model_name
device_info['serial_number'] = self.serial_number
device_info['meter_serial_number'] = self.meter_serial_number
device_info['meter_software_version'] = self.meter_software_version

if is_single_phase(self):
self._sensors = tuple(filter(self._single_phase_only, self.__all_sensors))
self._settings.update({s.id_: s for s in self.__settings_single_phase})
else:
self._settings.update({s.id_: s for s in self.__settings_three_phase})

if is_3_mppt(self):
# this is 3 PV strings inverter, keep all sensors
pass
else:
# this is only 2 PV strings inverter
self._sensors = tuple(filter(self._pv1_pv2_only, self._sensors))
if is_3_mppt(self):
pass
else:
self._sensors = tuple(filter(self._pv1_pv2_only, self._sensors))

except Exception as e:
print(f"Error reading device info: {e}")
device_info['error'] = str(e)

return device_info

async def read_runtime_data(self) -> dict[str, Any]:
response = await self._read_from_socket(self._READ_RUNNING_DATA)
Expand Down
11 changes: 11 additions & 0 deletions goodwe/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,17 @@ def read_value(self, data: ProtocolResponse):
return float(value) / 10 if value is not None else None


class Energy4W(Sensor):
"""Sensor representing meter energy [kWh] value encoded in 4 bytes"""

def __init__(self, id_: str, offset: int, name: str, kind: Optional[SensorKind]):
super().__init__(id_, offset, name, 4, "kWh", kind)

def read_value(self, data: ProtocolResponse):
value = read_bytes4(data)
return float(value) / 1000 if value is not None else None


class Energy8(Sensor):
"""Sensor representing energy [kWh] value encoded in 8 bytes"""

Expand Down