From 2a1182d5a6daca556d7e74bcbc6d116d51d3bb3c Mon Sep 17 00:00:00 2001 From: githubDante Date: Thu, 25 Apr 2024 20:01:24 +0300 Subject: [PATCH 01/11] Initial single-phase inverters mapping --- deye_controller/modbus/single_phase.py | 121 +++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 deye_controller/modbus/single_phase.py diff --git a/deye_controller/modbus/single_phase.py b/deye_controller/modbus/single_phase.py new file mode 100644 index 0000000..0ee194c --- /dev/null +++ b/deye_controller/modbus/single_phase.py @@ -0,0 +1,121 @@ +""" +MODBUS Registers for DEYE single phase inverters +""" +from .protocol import (LongType, LongUnsignedType, BoolType, DeviceTime, DeviceType, + DeviceSerialNumber, IntType, FloatType, RunState) + + +class DeviceTimeSingle(DeviceTime): + + def __init__(self): + super().__init__() + self.address = 22 + + +class OffsetValue(FloatType): + def __init__(self, address, name, scale=10, offset=1000, suffix=''): + super().__init__(address, name, scale, suffix=suffix) + self.offset = offset + + +class RunStateSingle(RunState): + + def __init__(self): + super().__init__() + self.address = 59 + + +class HoldingRegistersSingleCommon: + + DeviceType = DeviceType() + CommProtocol = IntType(1, 'modbus_address') + SerialNumber = DeviceSerialNumber() + RatedPower = IntType(8, 'rated_power') + + DeviceTime = DeviceTimeSingle() # Address 22 + 3 + MinimumInsulationImpedance = FloatType(25, 'minimum_insulation_impedance', 10, suffix='kOhm') + DCUpperLimit_V = FloatType(26, 'dc_voltage_upper_limit', 10, suffix='V') + + GridUpperLimit_A = FloatType(31, 'grid_current_upper_limit', 10, suffix='A') + + StartingUpperLimit_V = FloatType(32, 'starting_voltage_upper_limit', 10, suffix='V') + StartingLowerLimit_V = FloatType(33, 'starting_voltage_lower_limit', 10, suffix='V') + + InternalTempUpperLimit = FloatType(36, 'internal_temp_upper_limit', 10, suffix='°C') + + PFRegulation = OffsetValue(39, 'power_factor_regulation', scale=1000, offset=1000) + ActivePowerRegulation = FloatType(40, 'active_power_regulation', scale=10, suffix='%') + ReactivePowerRegulation = FloatType(41, 'reactive_power_regulation', scale=10, suffix='%') + ApparentPowerRegulation = FloatType(42, 'apparent_power_regulation', scale=10, suffix='%') + SwitchOnOff = BoolType(43, 'switch_on_off') + """ Factory reset skipped """ + + SelfCheck = BoolType(45, 'self_check_enabled') + IslandProtect = BoolType(46, 'island_protection_enabled') + + """ Some skipped for now """ + + RunState = RunStateSingle() # 59 + DailyActivePower = FloatType(60, 'daily_active_power', scale=10, suffix='kWh') + DailyReactivePower = FloatType(61, 'daily_reactive_power', scale=10, suffix='kWh') + DailyGridWorkingTime = IntType(62, 'daily_grid_working_time', suffix='s') + + TotalActivePower = LongType(63, 'total_active_power', scale=10, suffix='kWh') + """ #63 - probably Micro only """ + + +class HoldingRegistersSingleMicro(HoldingRegistersSingleCommon): + """ Single phase + Micro Inverter specific """ + + GridUpperLimit_V = FloatType(27, 'grid_voltage_upper_limit', 10, suffix='V') + GridLowerLimit_V = FloatType(28, 'grid_voltage_lower_limit', 10, suffix='V') + GridUpperLimit_F = FloatType(29, 'grid_frequency_upper_limit', 100, suffix='Hz') + GridLowerLimit_F = FloatType(30, 'grid_frequency_lower_limit', 100, suffix='Hz') + + OverFreqDeratePoint = FloatType(34, 'over_frequency_derate_point', 100, suffix='Hz') + OverFreqDerateRate = IntType(35, 'over_frequency_derate_rate', suffix='%') + + DailyPowerComponent1 = FloatType(65, 'daily_power_component_1', scale=10, suffix='kWh') + DailyPowerComponent2 = FloatType(66, 'daily_power_component_2', scale=10, suffix='kWh') + DailyPowerComponent3 = FloatType(67, 'daily_power_component_3', scale=10, suffix='kWh') + DailyPowerComponent4 = FloatType(68, 'daily_power_component_4', scale=10, suffix='kWh') + TotalPowerComponent1 = LongUnsignedType(69, 'total_power_component_1', scale=10, suffix='kWh') + TotalPowerComponent2 = LongUnsignedType(71, 'total_power_component_2', scale=10, suffix='kWh') + """ #73 not used """ + TotalPowerComponent3 = LongUnsignedType(74, 'total_power_component_3', scale=10, suffix='kWh') + TotalPowerComponent4 = LongUnsignedType(76, 'total_power_component_4', scale=10, suffix='kWh') + + """ !!! This one probably should be LongUnsigned !!! """ + + +class HoldingRegistersSingleHybrid(HoldingRegistersSingleCommon): + """ Single phase + Hybrid Inverter specific """ + + MonthlyPVPower = IntType(65, 'monthly_pv_power', suffix='kWh') + MonthlyLoadPower = IntType(66, 'monthly_load_power', suffix='kWh') + MonthlyGridPower = IntType(67, 'monthly_grid_power', suffix='kWh') + YearlyPVPower = LongUnsignedType(68, 'yearly_pv_power', scale=10, suffix='kWh') + DailyBatteryCharge = FloatType(70, 'daily_battery_charge', scale=10, suffix='kWh') + DailyBatteryDischarge = FloatType(71, 'daily_battery_discharge', scale=10, suffix='kWh') + TotalBatteryCharge = LongUnsignedType(72, 'total_battery_charge', 10, suffix='kWh') + TotalBatteryDischarge = LongUnsignedType(74, 'total_battery_discharge', 10, suffix='kWh') + + DailyGridBought = FloatType(76, 'daily_bought_from_grid', 10, suffix='kWh') + DailyGridSold = FloatType(77, 'daily_sold_to_grid', 10, suffix='kWh') + + +class HoldingRegistersSingleString(HoldingRegistersSingleCommon): + """ String inverter specific """ + + TotalReactivePower = LongType(65, 'total_reactive_power', scale=10, suffix='kVarh') + TotalWorkTime = LongType(67, 'total_work_time', scale=10, suffix='hours') + InverterEfficiency = FloatType(69, 'inverter_efficiency', scale=10, suffix='%') + GridVoltageAB = FloatType(70, 'grid_voltage_ab', scale=10, suffix='V') + GridVoltageBC = FloatType(71, 'grid_voltage_bc', scale=10, suffix='V') + GridVoltageAC = FloatType(72, 'grid_voltage_ac', scale=10, suffix='V') + GridVoltageA = FloatType(73, 'grid_voltage_a', scale=10, suffix='V') + GridVoltageB = FloatType(74, 'grid_voltage_b', scale=10, suffix='V') + GridVoltageC = FloatType(75, 'grid_voltage_c', scale=10, suffix='V') + GridCurrentA = FloatType(76, 'grid_current_a', scale=10, suffix='A') + GridCurrentB = FloatType(77, 'grid_current_b', scale=10, suffix='A') + GridCurrentC = FloatType(78, 'grid_current_b', scale=10, suffix='A') From ecbcdb5610fa933af991fe6b0803b9f1898c09d5 Mon Sep 17 00:00:00 2001 From: githubDante Date: Thu, 25 Apr 2024 20:11:51 +0300 Subject: [PATCH 02/11] InverterType moved. --- deye_controller/modbus/enums.py | 13 +++++++++++++ deye_controller/modbus/protocol.py | 13 ------------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/deye_controller/modbus/enums.py b/deye_controller/modbus/enums.py index 44ea63f..369dc38 100644 --- a/deye_controller/modbus/enums.py +++ b/deye_controller/modbus/enums.py @@ -1,6 +1,19 @@ import enum +class InverterType(int, enum.Enum): + + Inverter = 2 + Hybrid = 3 + Microinverter = 4 + Hybrid3Phase = 5 + Unknown = 0 + + @classmethod + def _missing_(cls, value): + return InverterType.Unknown + + class InverterState(int, enum.Enum): StandBy = 0 diff --git a/deye_controller/modbus/protocol.py b/deye_controller/modbus/protocol.py index bf9c962..aa61a91 100644 --- a/deye_controller/modbus/protocol.py +++ b/deye_controller/modbus/protocol.py @@ -11,19 +11,6 @@ from .enums import * -class InverterType(int, enum.Enum): - - Inverter = 2 - Hybrid = 3 - Microinverter = 4 - Hybrid3Phase = 5 - Unknown = 0 - - @classmethod - def _missing_(cls, value): - return InverterType.Unknown - - class Register(object): """ Base register of 2 bytes """ From 2649f874cf6a68bf015b7cf94e6e39d6174d9e68 Mon Sep 17 00:00:00 2001 From: githubDante Date: Thu, 25 Apr 2024 20:28:41 +0300 Subject: [PATCH 03/11] Replication of the as_list method --- deye_controller/modbus/single_phase.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/deye_controller/modbus/single_phase.py b/deye_controller/modbus/single_phase.py index 0ee194c..e989834 100644 --- a/deye_controller/modbus/single_phase.py +++ b/deye_controller/modbus/single_phase.py @@ -1,8 +1,9 @@ """ MODBUS Registers for DEYE single phase inverters """ +from typing import List from .protocol import (LongType, LongUnsignedType, BoolType, DeviceTime, DeviceType, - DeviceSerialNumber, IntType, FloatType, RunState) + DeviceSerialNumber, IntType, FloatType, RunState, Register) class DeviceTimeSingle(DeviceTime): @@ -85,7 +86,12 @@ class HoldingRegistersSingleMicro(HoldingRegistersSingleCommon): TotalPowerComponent3 = LongUnsignedType(74, 'total_power_component_3', scale=10, suffix='kWh') TotalPowerComponent4 = LongUnsignedType(76, 'total_power_component_4', scale=10, suffix='kWh') - """ !!! This one probably should be LongUnsigned !!! """ + @staticmethod + def as_list() -> List[Register]: + """ Method for easy iteration over the registers defined here """ + return [getattr(HoldingRegistersSingleMicro, x) for x in + HoldingRegistersSingleMicro.__dict__ if not x.startswith('_') + and not x.startswith('as_')] class HoldingRegistersSingleHybrid(HoldingRegistersSingleCommon): @@ -103,6 +109,13 @@ class HoldingRegistersSingleHybrid(HoldingRegistersSingleCommon): DailyGridBought = FloatType(76, 'daily_bought_from_grid', 10, suffix='kWh') DailyGridSold = FloatType(77, 'daily_sold_to_grid', 10, suffix='kWh') + @staticmethod + def as_list() -> List[Register]: + """ Method for easy iteration over the registers defined here """ + return [getattr(HoldingRegistersSingleHybrid, x) for x in + HoldingRegistersSingleHybrid.__dict__ if not x.startswith('_') + and not x.startswith('as_')] + class HoldingRegistersSingleString(HoldingRegistersSingleCommon): """ String inverter specific """ @@ -119,3 +132,10 @@ class HoldingRegistersSingleString(HoldingRegistersSingleCommon): GridCurrentA = FloatType(76, 'grid_current_a', scale=10, suffix='A') GridCurrentB = FloatType(77, 'grid_current_b', scale=10, suffix='A') GridCurrentC = FloatType(78, 'grid_current_b', scale=10, suffix='A') + + @staticmethod + def as_list() -> List[Register]: + """ Method for easy iteration over the registers defined here """ + return [getattr(HoldingRegistersSingleString, x) for x in + HoldingRegistersSingleString.__dict__ if not x.startswith('_') + and not x.startswith('as_')] From 78d90ddb63a5af1ebda030748d5865b6adc4ad46 Mon Sep 17 00:00:00 2001 From: githubDante Date: Thu, 25 Apr 2024 20:30:56 +0300 Subject: [PATCH 04/11] Expose what's needed. --- deye_controller/modbus/__init__.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/deye_controller/modbus/__init__.py b/deye_controller/modbus/__init__.py index e69de29..80c1a26 100644 --- a/deye_controller/modbus/__init__.py +++ b/deye_controller/modbus/__init__.py @@ -0,0 +1,12 @@ +from .protocol import HoldingRegisters, BatteryOnlyRegisters, WritableRegisters, TotalPowerOnly +from .single_phase import HoldingRegistersSingleHybrid, HoldingRegistersSingleString, HoldingRegistersSingleMicro +from .enums import InverterType + + +__all__ = [ + 'HoldingRegisters', 'BatteryOnlyRegisters', 'WritableRegisters', 'TotalPowerOnly', + 'HoldingRegistersSingleHybrid', 'HoldingRegistersSingleMicro', + 'HoldingRegistersSingleString', + 'InverterType', +] + From 2b5e0cfe2e26e62109b9aa9c6a1b16b02ee0a4ec Mon Sep 17 00:00:00 2001 From: githubDante Date: Thu, 25 Apr 2024 20:36:53 +0300 Subject: [PATCH 05/11] The common class is also useful --- deye_controller/modbus/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/deye_controller/modbus/__init__.py b/deye_controller/modbus/__init__.py index 80c1a26..63e907a 100644 --- a/deye_controller/modbus/__init__.py +++ b/deye_controller/modbus/__init__.py @@ -1,12 +1,13 @@ from .protocol import HoldingRegisters, BatteryOnlyRegisters, WritableRegisters, TotalPowerOnly -from .single_phase import HoldingRegistersSingleHybrid, HoldingRegistersSingleString, HoldingRegistersSingleMicro +from .single_phase import (HoldingRegistersSingleHybrid, HoldingRegistersSingleString, HoldingRegistersSingleMicro, + HoldingRegistersSingleCommon) from .enums import InverterType __all__ = [ 'HoldingRegisters', 'BatteryOnlyRegisters', 'WritableRegisters', 'TotalPowerOnly', 'HoldingRegistersSingleHybrid', 'HoldingRegistersSingleMicro', - 'HoldingRegistersSingleString', + 'HoldingRegistersSingleString', 'HoldingRegistersSingleCommon', 'InverterType', ] From cfd2289467abe06808e6723fce74ece7aa3ab2bb Mon Sep 17 00:00:00 2001 From: githubDante Date: Thu, 25 Apr 2024 20:54:01 +0300 Subject: [PATCH 06/11] deye-reader - Support for all inverter types - automatic inverter detection - auto switching to the correct registers set - battery/power/combo cmd switches work only with Hybrid3Phase, json & out options should work as expected --- deye_controller/deye_reader.py | 47 ++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/deye_controller/deye_reader.py b/deye_controller/deye_reader.py index a158107..1eac795 100644 --- a/deye_controller/deye_reader.py +++ b/deye_controller/deye_reader.py @@ -1,5 +1,7 @@ from pysolarmanv5 import PySolarmanV5, V5FrameError -from .modbus.protocol import HoldingRegisters, BatteryOnlyRegisters, TotalPowerOnly +from .modbus import (HoldingRegisters, BatteryOnlyRegisters, TotalPowerOnly, InverterType, + HoldingRegistersSingleCommon, HoldingRegistersSingleString, HoldingRegistersSingleHybrid, + HoldingRegistersSingleMicro) from .logger_scan import solar_scan from argparse import ArgumentParser from .utils import group_registers, map_response @@ -10,23 +12,40 @@ def read_inverter(address: str, logger_serial: int, batt_only=False, power_only= as_json=False, to_file=None): inv = PySolarmanV5(address, int(logger_serial), port=8899, mb_slave_id=1, verbose=False, socket_timeout=10, error_correction=True) + + type_detection = HoldingRegisters.DeviceType + type_detection.value = inv.read_holding_registers(type_detection.address, type_detection.len)[0] + inv_type = InverterType(type_detection.value) + + print(f'Detected inverter: {inv_type.name}') + iterator = [] js = {'logger': logger_serial, 'serial': 0, 'data': [] } - if batt_only: - iterator = [HoldingRegisters.SerialNumber] + BatteryOnlyRegisters - elif power_only: - iterator = [HoldingRegisters.SerialNumber] + TotalPowerOnly - elif combo: - iterator = [HoldingRegisters.SerialNumber] + BatteryOnlyRegisters - for reg in TotalPowerOnly: - if reg not in iterator: - iterator.append(reg) - - else: - iterator = HoldingRegisters.as_list() + + if inv_type == InverterType.Hybrid3Phase: + if batt_only: + iterator = [HoldingRegisters.SerialNumber] + BatteryOnlyRegisters + elif power_only: + iterator = [HoldingRegisters.SerialNumber] + TotalPowerOnly + elif combo: + iterator = [HoldingRegisters.SerialNumber] + BatteryOnlyRegisters + for reg in TotalPowerOnly: + if reg not in iterator: + iterator.append(reg) + + else: + iterator = HoldingRegisters.as_list() + + elif inv_type == InverterType.Hybrid: + iterator = HoldingRegistersSingleHybrid.as_list() + elif inv_type == InverterType.Microinverter: + iterator = HoldingRegistersSingleMicro.as_list() + elif inv_type == InverterType.Inverter: + iterator = HoldingRegistersSingleString.as_list() + reg_groups = group_registers(iterator) for group in reg_groups: res = inv.read_holding_registers(group.start_address, group.len) @@ -38,7 +57,7 @@ def read_inverter(address: str, logger_serial: int, batt_only=False, power_only= else: suffix = '' if as_json: - if reg == HoldingRegisters.SerialNumber: + if reg in [HoldingRegisters.SerialNumber, HoldingRegistersSingleCommon.SerialNumber]: js['serial'] = reg.format() else: js['data'].append({reg.description: {'addr': reg.address, 'value': reg.format(), 'unit': suffix}}) From a18c6d7c4e29fab9947f7e8bd5536c742932cbef Mon Sep 17 00:00:00 2001 From: githubDante Date: Mon, 13 May 2024 21:31:26 +0300 Subject: [PATCH 07/11] More single-phase registers - added handling for data not in sequential registers via a new type --- deye_controller/modbus/protocol.py | 20 +++++++++++ deye_controller/modbus/single_phase.py | 48 ++++++++++++++++++++++++-- 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/deye_controller/modbus/protocol.py b/deye_controller/modbus/protocol.py index aa61a91..f0f67ce 100644 --- a/deye_controller/modbus/protocol.py +++ b/deye_controller/modbus/protocol.py @@ -98,6 +98,26 @@ def format(self): return round(calculated / self.scale, 2) +class LongUnsignedHoleType(Register): + """ + For single phase inverters where the data is not in sequential registers + """ + + def __init__(self, address, name, scale, suffix=''): + super(LongUnsignedHoleType, self).__init__(address, 3, name) + self.scale = scale + self.suffix = suffix + + def format(self): + v = to_unsigned_bytes(self.value[::-2]) + calculated = int.from_bytes(v, byteorder='big') + if self.scale == 1: + return calculated + else: + return round(calculated / self.scale, 2) + + + class DeviceType(Register): def __init__(self): diff --git a/deye_controller/modbus/single_phase.py b/deye_controller/modbus/single_phase.py index e989834..e079fcc 100644 --- a/deye_controller/modbus/single_phase.py +++ b/deye_controller/modbus/single_phase.py @@ -3,7 +3,8 @@ """ from typing import List from .protocol import (LongType, LongUnsignedType, BoolType, DeviceTime, DeviceType, - DeviceSerialNumber, IntType, FloatType, RunState, Register) + DeviceSerialNumber, IntType, FloatType, RunState, Register, + LongUnsignedHoleType, TempWithOffset) class DeviceTimeSingle(DeviceTime): @@ -64,6 +65,19 @@ class HoldingRegistersSingleCommon: TotalActivePower = LongType(63, 'total_active_power', scale=10, suffix='kWh') """ #63 - probably Micro only """ + """ 91 """ + ACRadiatorTemp = TempWithOffset(91, 'ac_radiator_temperature', scale=10) + PowerFactor = FloatType(93, 'power_factor', scale=1) + """ Probably wrong value - needs research """ + AmbientTemp = FloatType(95, 'ambient_temperature', scale=10, suffix='°C') + PVPowerHistory = LongUnsignedType(96, 'pv_power_history', scale=10, suffix='kWh') + + # TODO: 100 through 106 - handling for Warning and Fault messages + CorrectedAH = IntType(107, 'corrected_Ah', suffix='Ah') + DailyPVPower = FloatType(108, 'daily_pv_power', scale=10, suffix='kWh') + + """ Debug & MI data up to #150 """ + class HoldingRegistersSingleMicro(HoldingRegistersSingleCommon): """ Single phase + Micro Inverter specific """ @@ -84,7 +98,19 @@ class HoldingRegistersSingleMicro(HoldingRegistersSingleCommon): TotalPowerComponent2 = LongUnsignedType(71, 'total_power_component_2', scale=10, suffix='kWh') """ #73 not used """ TotalPowerComponent3 = LongUnsignedType(74, 'total_power_component_3', scale=10, suffix='kWh') - TotalPowerComponent4 = LongUnsignedType(76, 'total_power_component_4', scale=10, suffix='kWh') + TotalPowerComponent4 = LongUnsignedType(77, 'total_power_component_4', scale=10, suffix='kWh') + GridFrequency = FloatType(79, 'grid_frequency', scale=100, suffix='Hz') + + DCTransformerTemp = FloatType(90, 'dc_transformer_temperature', scale=10, suffix='°C') + + DCVoltage_1 = FloatType(109, 'dc_voltage_1', scale=10, suffix='V') + DCCurrent_1 = FloatType(110, 'dc_current_1', scale=10, suffix='A') + DCVoltage_2 = FloatType(111, 'dc_voltage_2', scale=10, suffix='V') + DCCurrent_2 = FloatType(112, 'dc_current_2', scale=10, suffix='A') + DCVoltage_3 = FloatType(113, 'dc_voltage_3', scale=10, suffix='V') + DCCurrent_3 = FloatType(114, 'dc_current_3', scale=10, suffix='A') + DCVoltage_4 = FloatType(115, 'dc_voltage_4', scale=10, suffix='V') + DCCurrent_4 = FloatType(116, 'dc_current_4', scale=10, suffix='A') @staticmethod def as_list() -> List[Register]: @@ -109,6 +135,15 @@ class HoldingRegistersSingleHybrid(HoldingRegistersSingleCommon): DailyGridBought = FloatType(76, 'daily_bought_from_grid', 10, suffix='kWh') DailyGridSold = FloatType(77, 'daily_sold_to_grid', 10, suffix='kWh') + TotalGridInPower = LongUnsignedHoleType(78, 'total_grid_in_power', scale=10, suffix='kWh') + TotalGridOutPower = LongUnsignedType(81, 'total_grid_out_power', scale=10, suffix='kWh') + + GeneratorDailyTime = FloatType(83, 'gen_daily_operating_time', scale=10, suffix='hours') + TotalLoadPower = LongUnsignedType(85, 'total_load_power', scale=10, suffix='kWh') + AnnualLoadPower = LongUnsignedType(87, 'annual_load_power', scale=10, suffix='kWh') + AnnualGridOutPower = LongUnsignedType(98, 'annual_grid_out_power', scale=10, suffix='kWh') + + @staticmethod def as_list() -> List[Register]: """ Method for easy iteration over the registers defined here """ @@ -133,6 +168,15 @@ class HoldingRegistersSingleString(HoldingRegistersSingleCommon): GridCurrentB = FloatType(77, 'grid_current_b', scale=10, suffix='A') GridCurrentC = FloatType(78, 'grid_current_b', scale=10, suffix='A') + CurrentPower = LongUnsignedType(80, 'current_power', scale=10, suffix='W') + InputActivePower = LongUnsignedType(82, 'input_active_power', scale=10, suffix='W') + OutputApparentPower = LongUnsignedType(84, 'output_apparent_power', scale=10, suffix='W') + OutputActivePower = LongUnsignedType(86, 'output_active_power', scale=10, suffix='W') + OutputReactivePower = LongUnsignedType(88, 'output_reactive_power', scale=10, suffix='W') + + RCDLeakCurrent = FloatType(98, 'RCD_leak_current', scale=100, suffix='A') + PowerLimiter = FloatType(99, 'power_limiter', scale=1, suffix='W') + @staticmethod def as_list() -> List[Register]: """ Method for easy iteration over the registers defined here """ From 0c322d088db4cc7ec307cc210b425b6018773777 Mon Sep 17 00:00:00 2001 From: githubDante Date: Wed, 31 Jul 2024 22:10:09 +0300 Subject: [PATCH 08/11] String inverter - registers 150 to 210 --- deye_controller/modbus/single_phase.py | 47 ++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/deye_controller/modbus/single_phase.py b/deye_controller/modbus/single_phase.py index e079fcc..0d9e66e 100644 --- a/deye_controller/modbus/single_phase.py +++ b/deye_controller/modbus/single_phase.py @@ -177,6 +177,53 @@ class HoldingRegistersSingleString(HoldingRegistersSingleCommon): RCDLeakCurrent = FloatType(98, 'RCD_leak_current', scale=100, suffix='A') PowerLimiter = FloatType(99, 'power_limiter', scale=1, suffix='W') + """ String current """ + String_1_Current = FloatType(150, 'string_1_current', scale=10, suffix='A') + String_2_Current = FloatType(151, 'string_2_current', scale=10, suffix='A') + String_3_Current = FloatType(152, 'string_3_current', scale=10, suffix='A') + String_4_Current = FloatType(153, 'string_4_current', scale=10, suffix='A') + String_5_Current = FloatType(154, 'string_5_current', scale=10, suffix='A') + String_6_Current = FloatType(155, 'string_6_current', scale=10, suffix='A') + String_7_Current = FloatType(156, 'string_7_current', scale=10, suffix='A') + String_8_Current = FloatType(157, 'string_8_current', scale=10, suffix='A') + String_9_Current = FloatType(158, 'string_9_current', scale=10, suffix='A') + String_10_Current = FloatType(159, 'string_10_current', scale=10, suffix='A') + String_11_Current = FloatType(160, 'string_11_current', scale=10, suffix='A') + String_12_Current = FloatType(161, 'string_12_current', scale=10, suffix='A') + String_13_Current = FloatType(162, 'string_13_current', scale=10, suffix='A') + String_14_Current = FloatType(163, 'string_14_current', scale=10, suffix='A') + String_15_Current = FloatType(164, 'string_15_current', scale=10, suffix='A') + String_16_Current = FloatType(165, 'string_16_current', scale=10, suffix='A') + """ String power """ + String_1_Power = LongUnsignedType(166, 'string_1_power', scale=10, suffix='kWh') + String_2_Power = LongUnsignedType(168, 'string_2_power', scale=10, suffix='kWh') + String_3_Power = LongUnsignedType(170, 'string_3_power', scale=10, suffix='kWh') + String_4_Power = LongUnsignedType(172, 'string_4_power', scale=10, suffix='kWh') + String_5_Power = LongUnsignedType(174, 'string_5_power', scale=10, suffix='kWh') + String_6_Power = LongUnsignedType(176, 'string_6_power', scale=10, suffix='kWh') + String_7_Power = LongUnsignedType(178, 'string_7_power', scale=10, suffix='kWh') + String_8_Power = LongUnsignedType(180, 'string_8_power', scale=10, suffix='kWh') + String_9_Power = LongUnsignedType(182, 'string_9_power', scale=10, suffix='kWh') + String_10_Power = LongUnsignedType(184, 'string_10_power', scale=10, suffix='kWh') + String_11_Power = LongUnsignedType(186, 'string_11_power', scale=10, suffix='kWh') + String_12_Power = LongUnsignedType(188, 'string_12_power', scale=10, suffix='kWh') + String_13_Power = LongUnsignedType(190, 'string_13_power', scale=10, suffix='kWh') + String_14_Power = LongUnsignedType(192, 'string_14_power', scale=10, suffix='kWh') + String_15_Power = LongUnsignedType(194, 'string_15_power', scale=10, suffix='kWh') + String_16_Power = LongUnsignedType(196, 'string_16_power', scale=10, suffix='kWh') + + """ Daily & Total + Hist values probably should be renamed to Total + """ + LoadActivePower = LongUnsignedType(198, 'load_active_power', scale=1, suffix='W') + DailyLoadPower = FloatType(200, 'daily_load_power', scale=100, suffix='kWh') + HistActivePower = LongUnsignedType(201, 'history_active_power', scale=10, suffix='kWh') + MeterActivePower = LongType(203, 'meter_active_power', scale=10, suffix='kWh') + DailyGridSell = FloatType(205, 'daily_grid_sell', scale=100, suffix='kWh') + HistGridSell = LongUnsignedType(206, 'history_grid_sell', scale=10, suffix='kWh') + DailyGridBuy = FloatType(208, 'daily_grid_buy', scale=100, suffix='kWh') + HistGridBuy = LongUnsignedType(209, 'history_grid_buy', scale=10, suffix='kWh') + @staticmethod def as_list() -> List[Register]: """ Method for easy iteration over the registers defined here """ From dc0d4b8439d4b647750a304ec7729441690cbef4 Mon Sep 17 00:00:00 2001 From: githubDante Date: Thu, 10 Oct 2024 15:53:32 +0300 Subject: [PATCH 09/11] Inverter work mode - closes #16 Read/Write for register 142 --- deye_controller/modbus/enums.py | 17 +++++++++++++++++ deye_controller/modbus/protocol.py | 11 ++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/deye_controller/modbus/enums.py b/deye_controller/modbus/enums.py index 369dc38..0f6345b 100644 --- a/deye_controller/modbus/enums.py +++ b/deye_controller/modbus/enums.py @@ -185,3 +185,20 @@ def __format__(self, format_spec): def __str__(self): return self.name + +class WorkMode(int, enum.Enum): + """ + Register 142 - GH #16 + """ + + SellingFirst = 0 + ZeroExportToLoad = 1 + ZeroExportToCT = 2 + Unknown = -1 + + def __str__(self): + return self.name + + @classmethod + def _missing_(cls, value): + return WorkMode.Unknown diff --git a/deye_controller/modbus/protocol.py b/deye_controller/modbus/protocol.py index f0f67ce..47a93cd 100644 --- a/deye_controller/modbus/protocol.py +++ b/deye_controller/modbus/protocol.py @@ -352,6 +352,14 @@ def format(self): return str(GenPortMode(self.value)) +class InverterWorkMode(Register): + def __init__(self): + super().__init__(142, 1, 'work_mode') + + def format(self): + return str(WorkMode(self.value)) + + class HoldingRegisters: DeviceType = DeviceType() @@ -400,7 +408,7 @@ class HoldingRegisters: """ Smart Load control - need more info """ GeneratorPortSetup = GenPortUse() """ Smart Load """ - + IverterWorkMode = InverterWorkMode() GridExportLimit = IntType(143, 'grid_max_output_pwr', suffix='W') SolarSell = BoolType(145, 'solar_sell') SellTimeOfUse = TimeOfUseSell() @@ -868,6 +876,7 @@ class WritableRegisters: SmartLoadOnVoltage = FloatWritable(address=136, low_limit=38, high_limit=63, scale=100) SmartLoadOnCapacity = IntWritable(address=137, low_limit=0, high_limit=100) + InverterWorkMode = IntWritable(address=142, low_limit=0, high_limit=2) GridExportLimit = IntWritable(address=143, low_limit=0, high_limit=15000) SolarSell = BoolWritable(address=145) From 4c7eb9934f466f07409882c387a54056cef51ae2 Mon Sep 17 00:00:00 2001 From: githubDante Date: Thu, 10 Oct 2024 16:46:40 +0300 Subject: [PATCH 10/11] Hybrid & Micro 150 - 200 Registers from 150 to 200 for hybrid & micro inverters. --- deye_controller/__init__.py | 4 ++ deye_controller/modbus/enums.py | 13 ++++ deye_controller/modbus/protocol.py | 14 ++++- deye_controller/modbus/single_phase.py | 86 ++++++++++++++++++++++++-- 4 files changed, 110 insertions(+), 7 deletions(-) diff --git a/deye_controller/__init__.py b/deye_controller/__init__.py index 95b2ede..ba6c1e2 100644 --- a/deye_controller/__init__.py +++ b/deye_controller/__init__.py @@ -1,4 +1,8 @@ from .modbus.protocol import (HoldingRegisters, WritableRegisters, BatteryOnlyRegisters, TotalPowerOnly) +from .modbus.single_phase import (HoldingRegistersSingleHybrid, HoldingRegistersSingleMicro, + HoldingRegistersSingleString) + + from .sell_programmer import SellProgrammer diff --git a/deye_controller/modbus/enums.py b/deye_controller/modbus/enums.py index 0f6345b..f59bf00 100644 --- a/deye_controller/modbus/enums.py +++ b/deye_controller/modbus/enums.py @@ -202,3 +202,16 @@ def __str__(self): @classmethod def _missing_(cls, value): return WorkMode.Unknown + + +class Relay(int, enum.Enum): + Open = 0 + Closed = 1 + Error = -1 + + @classmethod + def _missing_(cls, value): + return Relay.Error + + def __format__(self, format_spec): + return self.name \ No newline at end of file diff --git a/deye_controller/modbus/protocol.py b/deye_controller/modbus/protocol.py index 47a93cd..5133cf9 100644 --- a/deye_controller/modbus/protocol.py +++ b/deye_controller/modbus/protocol.py @@ -237,8 +237,18 @@ def __init__(self): def format(self): return BatteryControlMode(self.value) - - + + +class BatteryTemp(Register): + + def __init__(self, address): + super(BatteryTemp, self).__init__(address, 1, 'battery_temp') + self.suffix = '°C' + + def format(self): + return round((self.value - 1000) / 10, 2) + + class BMSBatteryTemp(Register): def __init__(self): diff --git a/deye_controller/modbus/single_phase.py b/deye_controller/modbus/single_phase.py index 0d9e66e..0922b05 100644 --- a/deye_controller/modbus/single_phase.py +++ b/deye_controller/modbus/single_phase.py @@ -4,7 +4,20 @@ from typing import List from .protocol import (LongType, LongUnsignedType, BoolType, DeviceTime, DeviceType, DeviceSerialNumber, IntType, FloatType, RunState, Register, - LongUnsignedHoleType, TempWithOffset) + LongUnsignedHoleType, TempWithOffset, BatteryTemp) + +from .enums import Relay + + +class RelayStatus(Register): + """ + Relay status for single phase Hybrid & Micro - Reg: 194 + """ + def __init__(self): + super().__init__(194, 1, 'relay_status') + + def format(self): + return Relay(self.value) class DeviceTimeSingle(DeviceTime): @@ -79,7 +92,71 @@ class HoldingRegistersSingleCommon: """ Debug & MI data up to #150 """ -class HoldingRegistersSingleMicro(HoldingRegistersSingleCommon): +class HoldingRegistersSingleNoString: + """ + Common registers for Micro & Hybrid + """ + """ CT maybe """ + GridVoltageL1 = FloatType(150, 'grid_side_voltage_l1', scale=10, suffix='V') + GridVoltageL2 = FloatType(151, 'grid_side_voltage_l2', scale=10, suffix='V') + GridVoltageDiff = FloatType(152, 'grid_side_voltage_l1-l2', scale=10, suffix='V') + + """ Relay voltage """ + RelayVoltage = FloatType(153, 'voltage_at_the_relay', scale=10, suffix='V') + + InverterOutVoltageL1 = FloatType(154, 'inv_out_voltage_l1', scale=10, suffix='V') + InverterOutVoltageL2 = FloatType(155, 'inv_out_voltage_l2', scale=10, suffix='V') + InverterOutVoltageDiff = FloatType(156, 'inv_out_voltage_l1-l2', scale=10, suffix='V') + + LoadVoltageL1 = FloatType(157, 'load_voltage_l1', scale=10, suffix='V') + LoadVoltageL2 = FloatType(158, 'load_voltage_l1', scale=10, suffix='V') + + GridCurrentL1 = FloatType(160, 'grid_side_current_l1', scale=100, suffix='A', signed=True) + GridCurrentL2 = FloatType(161, 'grid_side_current_l2', scale=100, suffix='A', signed=True) + GridExtLimiterL1 = FloatType(162, 'grid_limiter_l1', scale=100, suffix='A', signed=True) + GridExtLimiterL2 = FloatType(163, 'grid_limiter_l2', scale=100, suffix='A', signed=True) + InverterCurrentL1 = FloatType(164, 'inverter_current_l1', scale=100, suffix='A', signed=True) + InverterCurrentL2 = FloatType(165, 'inverter_current_l2', scale=100, suffix='A', signed=True) + + GridPowerL1 = IntType(167, 'grid_side_power_l1', suffix='W', signed=True) + GridPowerL2 = IntType(168, 'grid_side_power_l1', suffix='W', signed=True) + GridPowerTotal = IntType(169, 'grid_side_total_power', suffix='W', signed=True) + + GridExtLimiterPwrL1 = IntType(170, 'grid_side_limiter_l1_power', suffix='W', signed=True) + GridExtLimiterPwrL2 = IntType(171, 'grid_side_limiter_l2_power', suffix='W', signed=True) + GridExtLimiterPwrT = IntType(172, 'grid_side_limiter_total_power', suffix='W', signed=True) + + InverterPowerL1 = IntType(173, 'inverter_power_l1', suffix='W', signed=True) + InverterPowerL2 = IntType(174, 'inverter_power_l2', suffix='W', signed=True) + InverterTotalPower = IntType(175, 'inverter_total_power', suffix='W', signed=True) + + LoadPowerL1 = IntType(176, 'load_power_l1', suffix='W', signed=True) + LoadPowerL2 = IntType(177, 'load_power_l2', suffix='W', signed=True) + LoadTotalPower = IntType(178, 'load_total_power', suffix='W', signed=True) + LoadCurrentL1 = FloatType(179, 'load_current_l1', scale=100, suffix='A', signed=True) + LoadCurrentL2 = FloatType(180, 'load_current_l2', scale=100, suffix='A', signed=True) + + """" Battery """ + BatteryTemp = BatteryTemp(182) + BatteryVoltage = FloatType(183, 'battery_voltage', scale=100, suffix='V') + BatterySOC = IntType(184, 'battery_SOC', suffix='%') + + """ PV """ + PV1Power = IntType(186, 'pv1_power', suffix='W') + PV2Power = IntType(187, 'pv2_power', suffix='W') + PV3Power = IntType(188, 'pv3_power', suffix='W') + PV4Power = IntType(189, 'pv4_power', suffix='W') + + BatteryPower = IntType(190, 'battery_power', suffix='W', signed=True) + BatteryCurrent = FloatType(191, 'battery_current', scale=100, suffix='A', signed=True) + + LoadFrequency = FloatType(192, 'load_frequency', scale=100, suffix='Hz') + InverterFrequency = FloatType(193, 'inverter_frequency', scale=100, suffix='Hz') + + RelayStatus = RelayStatus() + + +class HoldingRegistersSingleMicro(HoldingRegistersSingleCommon, HoldingRegistersSingleNoString): """ Single phase + Micro Inverter specific """ GridUpperLimit_V = FloatType(27, 'grid_voltage_upper_limit', 10, suffix='V') @@ -120,7 +197,7 @@ def as_list() -> List[Register]: and not x.startswith('as_')] -class HoldingRegistersSingleHybrid(HoldingRegistersSingleCommon): +class HoldingRegistersSingleHybrid(HoldingRegistersSingleCommon, HoldingRegistersSingleNoString): """ Single phase + Hybrid Inverter specific """ MonthlyPVPower = IntType(65, 'monthly_pv_power', suffix='kWh') @@ -143,7 +220,6 @@ class HoldingRegistersSingleHybrid(HoldingRegistersSingleCommon): AnnualLoadPower = LongUnsignedType(87, 'annual_load_power', scale=10, suffix='kWh') AnnualGridOutPower = LongUnsignedType(98, 'annual_grid_out_power', scale=10, suffix='kWh') - @staticmethod def as_list() -> List[Register]: """ Method for easy iteration over the registers defined here """ @@ -155,7 +231,7 @@ def as_list() -> List[Register]: class HoldingRegistersSingleString(HoldingRegistersSingleCommon): """ String inverter specific """ - TotalReactivePower = LongType(65, 'total_reactive_power', scale=10, suffix='kVarh') + TotalReactivePower = LongType(65, 'total_reactive_power', scale=10, suffix='kVarH') TotalWorkTime = LongType(67, 'total_work_time', scale=10, suffix='hours') InverterEfficiency = FloatType(69, 'inverter_efficiency', scale=10, suffix='%') GridVoltageAB = FloatType(70, 'grid_voltage_ab', scale=10, suffix='V') From 43b2bef50004fe6aeb1b7d260774d6876affa9c7 Mon Sep 17 00:00:00 2001 From: githubDante Date: Fri, 11 Oct 2024 21:36:22 +0300 Subject: [PATCH 11/11] Common registers 200-232 --- deye_controller/modbus/enums.py | 17 +++++- deye_controller/modbus/single_phase.py | 79 +++++++++++++++++++++++++- 2 files changed, 94 insertions(+), 2 deletions(-) diff --git a/deye_controller/modbus/enums.py b/deye_controller/modbus/enums.py index f59bf00..ee04971 100644 --- a/deye_controller/modbus/enums.py +++ b/deye_controller/modbus/enums.py @@ -214,4 +214,19 @@ def _missing_(cls, value): return Relay.Error def __format__(self, format_spec): - return self.name \ No newline at end of file + return self.name + + +class BatteryControlSingle(int, enum.Enum): + + Lead = 0 + Lithium = 1 + + Error = -1 + + def __format__(self, format_spec): + return self.name + + @classmethod + def _missing_(cls, value): + return BatteryControlSingle.Error \ No newline at end of file diff --git a/deye_controller/modbus/single_phase.py b/deye_controller/modbus/single_phase.py index 0922b05..9e2e5c9 100644 --- a/deye_controller/modbus/single_phase.py +++ b/deye_controller/modbus/single_phase.py @@ -6,7 +6,7 @@ DeviceSerialNumber, IntType, FloatType, RunState, Register, LongUnsignedHoleType, TempWithOffset, BatteryTemp) -from .enums import Relay +from .enums import Relay, BatteryControlSingle, BatteryControlMode, TwoBitState class RelayStatus(Register): @@ -38,6 +38,34 @@ class RunStateSingle(RunState): def __init__(self): super().__init__() self.address = 59 + + +class BatteryMode(Register): + + def __init__(self): + super().__init__(200, 1, 'battery_mode') + + def format(self): + return BatteryControlSingle(self.value) + + +class BatteryControl(Register): + def __init__(self): + super().__init__(213, 1,'battery_control_mode') + + def format(self): + return BatteryControlMode(self.value) + + +class LithiumWakeUpSingle(Register): + """ + Register: 214 + """ + def __init__(self): + super().__init__(214, 1, 'battery_wakeup') + + def format(self): + return TwoBitState(self.value) class HoldingRegistersSingleCommon: @@ -90,6 +118,55 @@ class HoldingRegistersSingleCommon: DailyPVPower = FloatType(108, 'daily_pv_power', scale=10, suffix='kWh') """ Debug & MI data up to #150 """ + """ String, Hybrid & micro separated """ + """ Next common 200 - not sure if applies to Micro """ + + """ R/W """ + BatteryMode = BatteryMode() + EqualizationV = FloatType(201, 'equalization_voltage', scale=10, suffix='V') + AbsorptionV = FloatType(202, 'absorption_voltage', scale=10, suffix='V') + FloatV = FloatType(203, 'absorption_voltage', scale=10, suffix='V') + BatteryCapacity = IntType(204, 'battery_capacity', suffix='Ah') + DisplayedSOC = IntType(205, 'LCD_displayed_SOC', suffix='%') + BatteryTempProtectionLow = OffsetValue(206, 'battery_low_temp_protection', scale=10, + offset=1000, suffix='°C') + + EqualizationCycle = IntType(207, 'equalization_cycle', suffix='days') + EqualizationTime = IntType(208, 'equalization_time', suffix='half-hour') + TEMPCO = IntType(209, 'TEMPCO', suffix='mv/°C') + + MaxChargeCurrent = IntType(210, 'max_charge_current', suffix='A') + MaxDischargeCurrent = IntType(211, 'max_discharge_current', suffix='A') + + BatteryControl = BatteryControl() + BatteryWakeup = LithiumWakeUpSingle() + BatteryResistance = IntType(215, 'battery_resistance', suffix='mOhm') + BatterChargingEff = FloatType(216, 'battery_charge_efficiency', suffix='%', scale=10) + BatterySocShutDown = IntType(217, 'battery_shutdown_soc', suffix='%') + BatterySocRestart = IntType(218, 'battery_restart_soc', suffix='%') + BatterySocLow = IntType(219, 'battery_low_soc', suffix='%') + + BatteryVShutDown = FloatType(220, 'battery_shutdown_voltage', suffix='V', scale=100) + BatteryVRestart = FloatType(221, 'battery_restart_voltage', suffix='V', scale=100) + BatteryVLow = FloatType(222, 'battery_low_voltage', suffix='V', scale=100) + + GeneratorMaxTime = FloatType(223, 'gen_max_work_time', suffix='hour(s)', scale=10) + GeneratorCoolingTime = FloatType(224, 'gen_cooling_time', suffix='hour(s)', scale=10) + + GeneratorChargingVPoint = FloatType(225, 'gen_charge_start_v', scale=100, suffix='V') + GeneratorChargingSOCPoint = IntType(226, 'gen_charge_start_soc', suffix='%') + GeneratorChargeCurrent = IntType(227, 'gen_charge_current_limit', suffix='A') + + GridChargingVPoint = FloatType(228, 'grid_charge_start_v', scale=100, suffix='V') + GridChargingSOCPoint = IntType(229, 'grid_charge_start_soc', suffix='%') + GridChargeCurrent = IntType(230, 'grid_charge_current_limit', suffix='A') + + GeneratorChargeEnabled = BoolType(231, 'gen_charge_enabled') + GridChargeEnabled = BoolType(232, 'grid_charge_enabled') + # TODO: continue + + + class HoldingRegistersSingleNoString: