From d592b2a3004ffec13c63b7f9deaa9c862c3a7c3a Mon Sep 17 00:00:00 2001 From: mle Date: Sat, 16 Dec 2023 14:14:32 +0100 Subject: [PATCH] Redesign sensor definitions and loading. Declare real modbus registers in sensors instead of offset of some first sensor. Delegate seeking and reading to ProtocolResponse, so modbus values can be potentially read individually and/or in smaller consecutive chunks. --- goodwe/dt.py | 143 ++++++------ goodwe/es.py | 6 +- goodwe/et.py | 540 +++++++++++++++++++++---------------------- goodwe/inverter.py | 24 +- goodwe/protocol.py | 25 +- goodwe/sensor.py | 130 +++++++---- tests/test_sensor.py | 65 ++++-- 7 files changed, 498 insertions(+), 435 deletions(-) diff --git a/goodwe/dt.py b/goodwe/dt.py index 0959e47..c49db68 100644 --- a/goodwe/dt.py +++ b/goodwe/dt.py @@ -15,21 +15,21 @@ class DT(Inverter): """Class representing inverter of DT/MS/D-NS/XS or GE's GEP(PSB/PSC) families""" __all_sensors: Tuple[Sensor, ...] = ( - Timestamp("timestamp", 0, "Timestamp"), - Voltage("vpv1", 6, "PV1 Voltage", Kind.PV), - Current("ipv1", 8, "PV1 Current", Kind.PV), + Timestamp("timestamp", 30100, "Timestamp"), + Voltage("vpv1", 30103, "PV1 Voltage", Kind.PV), + Current("ipv1", 30104, "PV1 Current", Kind.PV), Calculated("ppv1", - lambda data: round(read_voltage(data, 6) * read_current(data, 8)), + lambda data: round(read_voltage(data, 30103) * read_current(data, 30104)), "PV1 Power", "W", Kind.PV), - Voltage("vpv2", 10, "PV2 Voltage", Kind.PV), - Current("ipv2", 12, "PV2 Current", Kind.PV), + Voltage("vpv2", 30105, "PV2 Voltage", Kind.PV), + Current("ipv2", 30106, "PV2 Current", Kind.PV), Calculated("ppv2", - lambda data: round(read_voltage(data, 10) * read_current(data, 12)), + lambda data: round(read_voltage(data, 30105) * read_current(data, 30106)), "PV2 Power", "W", Kind.PV), - Voltage("vpv3", 14, "PV3 Voltage", Kind.PV), - Current("ipv3", 16, "PV3 Current", Kind.PV), + Voltage("vpv3", 30107, "PV3 Voltage", Kind.PV), + Current("ipv3", 30108, "PV3 Current", Kind.PV), Calculated("ppv3", - lambda data: round(read_voltage(data, 14) * read_current(data, 16)), + lambda data: round(read_voltage(data, 30107) * read_current(data, 30108)), "PV3 Power", "W", Kind.PV), # Voltage("vpv4", 14, "PV4 Voltage", Kind.PV), # Current("ipv4", 16, "PV4 Current", Kind.PV), @@ -37,72 +37,72 @@ class DT(Inverter): # Current("ipv5", 16, "PV5 Current", Kind.PV), # Voltage("vpv6", 14, "PV6 Voltage", Kind.PV), # Current("ipv6", 16, "PV7 Current", Kind.PV), - Voltage("vline1", 30, "On-grid L1-L2 Voltage", Kind.AC), - Voltage("vline2", 32, "On-grid L2-L3 Voltage", Kind.AC), - Voltage("vline3", 34, "On-grid L3-L1 Voltage", Kind.AC), - Voltage("vgrid1", 36, "On-grid L1 Voltage", Kind.AC), - Voltage("vgrid2", 38, "On-grid L2 Voltage", Kind.AC), - Voltage("vgrid3", 40, "On-grid L3 Voltage", Kind.AC), - Current("igrid1", 42, "On-grid L1 Current", Kind.AC), - Current("igrid2", 44, "On-grid L2 Current", Kind.AC), - Current("igrid3", 46, "On-grid L3 Current", Kind.AC), - Frequency("fgrid1", 48, "On-grid L1 Frequency", Kind.AC), - Frequency("fgrid2", 50, "On-grid L2 Frequency", Kind.AC), - Frequency("fgrid3", 52, "On-grid L3 Frequency", Kind.AC), + Voltage("vline1", 30115, "On-grid L1-L2 Voltage", Kind.AC), + Voltage("vline2", 30116, "On-grid L2-L3 Voltage", Kind.AC), + Voltage("vline3", 30117, "On-grid L3-L1 Voltage", Kind.AC), + Voltage("vgrid1", 30118, "On-grid L1 Voltage", Kind.AC), + Voltage("vgrid2", 30119, "On-grid L2 Voltage", Kind.AC), + Voltage("vgrid3", 30120, "On-grid L3 Voltage", Kind.AC), + Current("igrid1", 30121, "On-grid L1 Current", Kind.AC), + Current("igrid2", 30122, "On-grid L2 Current", Kind.AC), + Current("igrid3", 30123, "On-grid L3 Current", Kind.AC), + Frequency("fgrid1", 30124, "On-grid L1 Frequency", Kind.AC), + Frequency("fgrid2", 30125, "On-grid L2 Frequency", Kind.AC), + Frequency("fgrid3", 30126, "On-grid L3 Frequency", Kind.AC), Calculated("pgrid1", - lambda data: round(read_voltage(data, 36) * read_current(data, 42)), + lambda data: round(read_voltage(data, 30118) * read_current(data, 30121)), "On-grid L1 Power", "W", Kind.AC), Calculated("pgrid2", - lambda data: round(read_voltage(data, 38) * read_current(data, 44)), + lambda data: round(read_voltage(data, 30119) * read_current(data, 30122)), "On-grid L2 Power", "W", Kind.AC), Calculated("pgrid3", - lambda data: round(read_voltage(data, 40) * read_current(data, 46)), + lambda data: round(read_voltage(data, 30120) * read_current(data, 30123)), "On-grid L3 Power", "W", Kind.AC), - Integer("xx54", 54, "Unknown sensor@54"), - Power("ppv", 56, "PV Power", Kind.PV), - Integer("work_mode", 58, "Work Mode code"), - Enum2("work_mode_label", 58, WORK_MODES, "Work Mode"), - Long("error_codes", 60, "Error Codes"), - Integer("warning_code", 64, "Warning code"), - Integer("xx66", 66, "Unknown sensor@66"), - Integer("xx68", 68, "Unknown sensor@68"), - Integer("xx70", 70, "Unknown sensor@70"), - Integer("xx72", 72, "Unknown sensor@72"), - Integer("xx74", 74, "Unknown sensor@74"), - Integer("xx76", 76, "Unknown sensor@76"), - Integer("xx78", 78, "Unknown sensor@78"), - Integer("xx80", 80, "Unknown sensor@80"), - Temp("temperature", 82, "Inverter Temperature", Kind.AC), - Integer("xx84", 84, "Unknown sensor@84"), - Integer("xx86", 86, "Unknown sensor@86"), - Energy("e_day", 88, "Today's PV Generation", Kind.PV), - Energy4("e_total", 90, "Total PV Generation", Kind.PV), - Long("h_total", 94, "Hours Total", "h", Kind.PV), - Integer("safety_country", 98, "Safety Country code", "", Kind.AC), - Enum2("safety_country_label", 98, SAFETY_COUNTRIES, "Safety Country", Kind.AC), - Integer("xx100", 100, "Unknown sensor@100"), - Integer("xx102", 102, "Unknown sensor@102"), - Integer("xx104", 104, "Unknown sensor@104"), - Integer("xx106", 106, "Unknown sensor@106"), - Integer("xx108", 108, "Unknown sensor@108"), - Integer("xx110", 110, "Unknown sensor@110"), - Integer("xx112", 112, "Unknown sensor@112"), - Integer("xx114", 114, "Unknown sensor@114"), - Integer("xx116", 116, "Unknown sensor@116"), - Integer("xx118", 118, "Unknown sensor@118"), - Integer("xx120", 120, "Unknown sensor@120"), - Integer("xx122", 122, "Unknown sensor@122"), - Integer("funbit", 124, "FunBit", "", Kind.PV), - Voltage("vbus", 126, "Bus Voltage", Kind.PV), - Voltage("vnbus", 128, "NBus Voltage", Kind.PV), - Integer("xx130", 130, "Unknown sensor@130"), - Integer("xx132", 132, "Unknown sensor@132"), - Integer("xx134", 134, "Unknown sensor@134"), - Integer("xx136", 136, "Unknown sensor@136"), - Integer("xx138", 138, "Unknown sensor@138"), - Integer("xx140", 140, "Unknown sensor@140"), - Integer("xx142", 142, "Unknown sensor@142"), - Integer("xx144", 144, "Unknown sensor@144"), + Integer("xx54", 30127, "Unknown sensor@54"), + Power("ppv", 30128, "PV Power", Kind.PV), + 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"), + Integer("xx66", 30133, "Unknown sensor@66"), + Integer("xx68", 30134, "Unknown sensor@68"), + Integer("xx70", 30135, "Unknown sensor@70"), + Integer("xx72", 30136, "Unknown sensor@72"), + Integer("xx74", 30137, "Unknown sensor@74"), + Integer("xx76", 30138, "Unknown sensor@76"), + Integer("xx78", 30139, "Unknown sensor@78"), + Integer("xx80", 30140, "Unknown sensor@80"), + Temp("temperature", 30141, "Inverter Temperature", Kind.AC), + Integer("xx84", 30142, "Unknown sensor@84"), + Integer("xx86", 30143, "Unknown sensor@86"), + 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), + Integer("safety_country", 30149, "Safety Country code", "", Kind.AC), + Enum2("safety_country_label", 30149, SAFETY_COUNTRIES, "Safety Country", Kind.AC), + Integer("xx100", 30150, "Unknown sensor@100"), + Integer("xx102", 30151, "Unknown sensor@102"), + Integer("xx104", 30152, "Unknown sensor@104"), + Integer("xx106", 30153, "Unknown sensor@106"), + Integer("xx108", 30154, "Unknown sensor@108"), + Integer("xx110", 30155, "Unknown sensor@110"), + Integer("xx112", 30156, "Unknown sensor@112"), + Integer("xx114", 30157, "Unknown sensor@114"), + Integer("xx116", 30158, "Unknown sensor@116"), + Integer("xx118", 30159, "Unknown sensor@118"), + Integer("xx120", 30160, "Unknown sensor@120"), + Integer("xx122", 30161, "Unknown sensor@122"), + Integer("funbit", 30162, "FunBit", "", Kind.PV), + Voltage("vbus", 30163, "Bus Voltage", Kind.PV), + Voltage("vnbus", 30164, "NBus Voltage", Kind.PV), + Integer("xx130", 30165, "Unknown sensor@130"), + Integer("xx132", 30166, "Unknown sensor@132"), + Integer("xx134", 30167, "Unknown sensor@134"), + Integer("xx136", 30168, "Unknown sensor@136"), + Integer("xx138", 30169, "Unknown sensor@138"), + Integer("xx140", 30170, "Unknown sensor@140"), + Integer("xx142", 30171, "Unknown sensor@142"), + Integer("xx144", 30172, "Unknown sensor@144"), ) # Modbus registers of inverter settings, offsets are modbus register addresses @@ -183,8 +183,7 @@ async def read_setting(self, setting_id: str) -> Any: raise ValueError(f'Unknown setting "{setting_id}"') count = (setting.size_ + (setting.size_ % 2)) // 2 response = await self._read_from_socket(ModbusReadCommand(self.comm_addr, setting.offset, count)) - with io.BytesIO(response.response_data()) as buffer: - return setting.read_value(buffer) + return setting.read_value(response) async def write_setting(self, setting_id: str, value: Any): setting = self._settings.get(setting_id) diff --git a/goodwe/es.py b/goodwe/es.py index ded05ea..91a9ec4 100644 --- a/goodwe/es.py +++ b/goodwe/es.py @@ -223,12 +223,10 @@ async def read_setting(self, setting_id: str) -> Any: count = (setting.size_ + (setting.size_ % 2)) // 2 if self._is_modbus_setting(setting): response = await self._read_from_socket(ModbusReadCommand(self.comm_addr, setting.offset, count)) - with io.BytesIO(response.response_data()) as buffer: - return setting.read_value(buffer) + return setting.read_value(response) else: response = await self._read_from_socket(Aa55ReadCommand(setting.offset, count)) - with io.BytesIO(response.response_data()) as buffer: - return setting.read_value(buffer) + return setting.read_value(response) else: all_settings = await self.read_settings_data() return all_settings.get(setting_id) diff --git a/goodwe/et.py b/goodwe/et.py index ef08462..dd43e0c 100644 --- a/goodwe/et.py +++ b/goodwe/et.py @@ -19,312 +19,313 @@ class ET(Inverter): # Modbus registers from offset 0x891c (35100), count 0x7d (125) __all_sensors: Tuple[Sensor, ...] = ( - Timestamp("timestamp", 0, "Timestamp"), # 35100 - Voltage("vpv1", 6, "PV1 Voltage", Kind.PV), # 35103 - Current("ipv1", 8, "PV1 Current", Kind.PV), # 35104 - Power4("ppv1", 10, "PV1 Power", Kind.PV), # 35105 - Voltage("vpv2", 14, "PV2 Voltage", Kind.PV), # 35107 - Current("ipv2", 16, "PV2 Current", Kind.PV), # 35108 - Power4("ppv2", 18, "PV2 Power", Kind.PV), # 35109 - Voltage("vpv3", 22, "PV3 Voltage", Kind.PV), # 35111 - Current("ipv3", 24, "PV3 Current", Kind.PV), # 35112 - Power4("ppv3", 26, "PV3 Power", Kind.PV), # 35113 - Voltage("vpv4", 30, "PV4 Voltage", Kind.PV), # 35115 - Current("ipv4", 32, "PV4 Current", Kind.PV), # 35116 - Power4("ppv4", 34, "PV4 Power", Kind.PV), # 35117 + Timestamp("timestamp", 35100, "Timestamp"), + Voltage("vpv1", 35103, "PV1 Voltage", Kind.PV), + Current("ipv1", 35104, "PV1 Current", Kind.PV), + Power4("ppv1", 35105, "PV1 Power", Kind.PV), + Voltage("vpv2", 35107, "PV2 Voltage", Kind.PV), + Current("ipv2", 35108, "PV2 Current", Kind.PV), + Power4("ppv2", 35109, "PV2 Power", Kind.PV), + Voltage("vpv3", 35111, "PV3 Voltage", Kind.PV), + Current("ipv3", 35112, "PV3 Current", Kind.PV), + Power4("ppv3", 35113, "PV3 Power", Kind.PV), + Voltage("vpv4", 35115, "PV4 Voltage", Kind.PV), + Current("ipv4", 35116, "PV4 Current", Kind.PV), + Power4("ppv4", 35117, "PV4 Power", Kind.PV), # ppv1 + ppv2 + ppv3 + ppv4 Calculated("ppv", lambda data: - max(0, read_bytes4(data, 10)) + - max(0, read_bytes4(data, 18)) + - max(0, read_bytes4(data, 26)) + - max(0, read_bytes4(data, 34)), - "PV Power", "W", Kind.PV), # 35119 - Byte("pv4_mode", 38, "PV4 Mode code", "", Kind.PV), # 35120 l - Enum("pv4_mode_label", 38, PV_MODES, "PV4 Mode", Kind.PV), - Byte("pv3_mode", 39, "PV3 Mode code", "", Kind.PV), # 35120 h - Enum("pv3_mode_label", 39, PV_MODES, "PV3 Mode", Kind.PV), - Byte("pv2_mode", 40, "PV2 Mode code", "", Kind.PV), # 35119 l - Enum("pv2_mode_label", 40, PV_MODES, "PV2 Mode", Kind.PV), - Byte("pv1_mode", 41, "PV1 Mode code", "", Kind.PV), # 35119 h - Enum("pv1_mode_label", 41, PV_MODES, "PV1 Mode", Kind.PV), - Voltage("vgrid", 42, "On-grid L1 Voltage", Kind.AC), # 35121 - Current("igrid", 44, "On-grid L1 Current", Kind.AC), # 35122 - Frequency("fgrid", 46, "On-grid L1 Frequency", Kind.AC), # 35123 - # 48 reserved - Power("pgrid", 50, "On-grid L1 Power", Kind.AC), # 35125 - Voltage("vgrid2", 52, "On-grid L2 Voltage", Kind.AC), # 35126 - Current("igrid2", 54, "On-grid L2 Current", Kind.AC), # 35127 - Frequency("fgrid2", 56, "On-grid L2 Frequency", Kind.AC), # 35128 - # 58 reserved - Power("pgrid2", 60, "On-grid L2 Power", Kind.AC), # 35130 - Voltage("vgrid3", 62, "On-grid L3 Voltage", Kind.AC), # 35131 - Current("igrid3", 64, "On-grid L3 Current", Kind.AC), # 35132 - Frequency("fgrid3", 66, "On-grid L3 Frequency", Kind.AC), # 35133 - # 68 reserved - Power("pgrid3", 70, "On-grid L3 Power", Kind.AC), # 35135 - Integer("grid_mode", 72, "Grid Mode code", "", Kind.PV), # 35136 - Enum2("grid_mode_label", 72, GRID_MODES, "Grid Mode", Kind.PV), - # 74 reserved - Power("total_inverter_power", 76, "Total Power", Kind.AC), # 35138 - # 78 reserved - Power("active_power", 80, "Active Power", Kind.GRID), # 35140 + max(0, read_bytes4(data, 35105)) + + max(0, read_bytes4(data, 35109)) + + max(0, read_bytes4(data, 35113)) + + max(0, read_bytes4(data, 35117)), + "PV Power", "W", Kind.PV), + ByteH("pv4_mode", 35119, "PV4 Mode code", "", Kind.PV), # l + EnumL("pv4_mode_label", 35119, PV_MODES, "PV4 Mode", Kind.PV), + ByteH("pv3_mode", 35119, "PV3 Mode code", "", Kind.PV), # h + EnumH("pv3_mode_label", 35119, PV_MODES, "PV3 Mode", Kind.PV), + ByteL("pv2_mode", 35120, "PV2 Mode code", "", Kind.PV), # l + EnumL("pv2_mode_label", 35120, PV_MODES, "PV2 Mode", Kind.PV), + ByteH("pv1_mode", 35120, "PV1 Mode code", "", Kind.PV), # h + EnumH("pv1_mode_label", 35120, PV_MODES, "PV1 Mode", Kind.PV), + Voltage("vgrid", 35121, "On-grid L1 Voltage", Kind.AC), + Current("igrid", 35122, "On-grid L1 Current", Kind.AC), + Frequency("fgrid", 35123, "On-grid L1 Frequency", Kind.AC), + # 35124 reserved + Power("pgrid", 35125, "On-grid L1 Power", Kind.AC), + Voltage("vgrid2", 35126, "On-grid L2 Voltage", Kind.AC), + Current("igrid2", 35127, "On-grid L2 Current", Kind.AC), + Frequency("fgrid2", 35128, "On-grid L2 Frequency", Kind.AC), + # 35129 reserved + Power("pgrid2", 35130, "On-grid L2 Power", Kind.AC), + Voltage("vgrid3", 35131, "On-grid L3 Voltage", Kind.AC), + Current("igrid3", 35132, "On-grid L3 Current", Kind.AC), + Frequency("fgrid3", 35133, "On-grid L3 Frequency", Kind.AC), + # 35134 reserved + Power("pgrid3", 35135, "On-grid L3 Power", Kind.AC), + Integer("grid_mode", 35136, "Grid Mode code", "", Kind.PV), + Enum2("grid_mode_label", 35136, GRID_MODES, "Grid Mode", Kind.PV), + # 35137 reserved + Power("total_inverter_power", 35138, "Total Power", Kind.AC), + # 35139 reserved + Power("active_power", 35140, "Active Power", Kind.GRID), Calculated("grid_in_out", - lambda data: read_grid_mode(data, 80), + lambda data: read_grid_mode(data, 35140), "On-grid Mode code", "", Kind.GRID), EnumCalculated("grid_in_out_label", - lambda data: read_grid_mode(data, 80), GRID_IN_OUT_MODES, + lambda data: read_grid_mode(data, 35140), GRID_IN_OUT_MODES, "On-grid Mode", Kind.GRID), - # 82 reserved - Reactive("reactive_power", 84, "Reactive Power", Kind.GRID), # 35142 - # 86 reserved - Apparent("apparent_power", 88, "Apparent Power", Kind.GRID), # 35144 - Voltage("backup_v1", 90, "Back-up L1 Voltage", Kind.UPS), # 35145 - Current("backup_i1", 92, "Back-up L1 Current", Kind.UPS), # 35146 - Frequency("backup_f1", 94, "Back-up L1 Frequency", Kind.UPS), # 35147 - Integer("load_mode1", 96, "Load Mode L1"), # 35148 - # 98 reserved - Power("backup_p1", 100, "Back-up L1 Power", Kind.UPS), # 35150 - Voltage("backup_v2", 102, "Back-up L2 Voltage", Kind.UPS), # 35151 - Current("backup_i2", 104, "Back-up L2 Current", Kind.UPS), # 35152 - Frequency("backup_f2", 106, "Back-up L2 Frequency", Kind.UPS), # 35153 - Integer("load_mode2", 108, "Load Mode L2"), # 35154 - # 110 reserved - Power("backup_p2", 112, "Back-up L2 Power", Kind.UPS), # 35156 - Voltage("backup_v3", 114, "Back-up L3 Voltage", Kind.UPS), # 35157 - Current("backup_i3", 116, "Back-up L3 Current", Kind.UPS), # 35158 - Frequency("backup_f3", 118, "Back-up L3 Frequency", Kind.UPS), # 35159 - Integer("load_mode3", 120, "Load Mode L3"), # 35160 - # 122 reserved - Power("backup_p3", 124, "Back-up L3 Power", Kind.UPS), # 35162 - # 126 reserved - Power("load_p1", 128, "Load L1", Kind.AC), # 35164 - # 130 reserved - Power("load_p2", 132, "Load L2", Kind.AC), # 35166 - # 134 reserved - Power("load_p3", 136, "Load L3", Kind.AC), # 35168 - # 138 reserved - Power("backup_ptotal", 140, "Back-up Load", Kind.UPS), # 35170 - # 142 reserved - Power("load_ptotal", 144, "Load", Kind.AC), # 35172 - Integer("ups_load", 146, "Ups Load", "%", Kind.UPS), # 35173 - Temp("temperature_air", 148, "Inverter Temperature (Air)", Kind.AC), # 35174 - Temp("temperature_module", 150, "Inverter Temperature (Module)"), # 35175 - Temp("temperature", 152, "Inverter Temperature (Radiator)", Kind.AC), # 35176 - Integer("function_bit", 154, "Function Bit"), # 35177 - Voltage("bus_voltage", 156, "Bus Voltage", None), # 35178 - Voltage("nbus_voltage", 158, "NBus Voltage", None), # 35179 - Voltage("vbattery1", 160, "Battery Voltage", Kind.BAT), # 35180 - Current("ibattery1", 162, "Battery Current", Kind.BAT), # 35181 + # 35141 reserved + Reactive("reactive_power", 35142, "Reactive Power", Kind.GRID), + # 35143 reserved + Apparent("apparent_power", 35144, "Apparent Power", Kind.GRID), + Voltage("backup_v1", 35145, "Back-up L1 Voltage", Kind.UPS), + Current("backup_i1", 35146, "Back-up L1 Current", Kind.UPS), + Frequency("backup_f1", 35147, "Back-up L1 Frequency", Kind.UPS), + Integer("load_mode1", 35148, "Load Mode L1"), + # 35149 reserved + Power("backup_p1", 35150, "Back-up L1 Power", Kind.UPS), + Voltage("backup_v2", 35151, "Back-up L2 Voltage", Kind.UPS), + Current("backup_i2", 35152, "Back-up L2 Current", Kind.UPS), + Frequency("backup_f2", 35153, "Back-up L2 Frequency", Kind.UPS), + Integer("load_mode2", 35154, "Load Mode L2"), + # 35155 reserved + Power("backup_p2", 35156, "Back-up L2 Power", Kind.UPS), + Voltage("backup_v3", 35157, "Back-up L3 Voltage", Kind.UPS), + Current("backup_i3", 35158, "Back-up L3 Current", Kind.UPS), + Frequency("backup_f3", 35159, "Back-up L3 Frequency", Kind.UPS), + Integer("load_mode3", 35160, "Load Mode L3"), + # 35161 reserved + Power("backup_p3", 35162, "Back-up L3 Power", Kind.UPS), + # 35163 reserved + Power("load_p1", 35164, "Load L1", Kind.AC), + # 35165 reserved + Power("load_p2", 35166, "Load L2", Kind.AC), + # 35167 reserved + Power("load_p3", 35168, "Load L3", Kind.AC), + # 35169 reserved + Power("backup_ptotal", 35170, "Back-up Load", Kind.UPS), + # 35171 reserved + Power("load_ptotal", 35172, "Load", Kind.AC), + Integer("ups_load", 35173, "Ups Load", "%", Kind.UPS), + Temp("temperature_air", 35174, "Inverter Temperature (Air)", Kind.AC), + Temp("temperature_module", 35175, "Inverter Temperature (Module)"), + Temp("temperature", 35176, "Inverter Temperature (Radiator)", Kind.AC), + Integer("function_bit", 35177, "Function Bit"), + Voltage("bus_voltage", 35178, "Bus Voltage", None), + Voltage("nbus_voltage", 35179, "NBus Voltage", None), + Voltage("vbattery1", 35180, "Battery Voltage", Kind.BAT), + Current("ibattery1", 35181, "Battery Current", Kind.BAT), # round(vbattery1 * ibattery1), Calculated("pbattery1", - lambda data: round(read_voltage(data, 160) * read_current(data, 162)), - "Battery Power", "W", Kind.BAT), # 35182+35183 ? - Integer("battery_mode", 168, "Battery Mode code", "", Kind.BAT), # 35184 - Enum2("battery_mode_label", 168, BATTERY_MODES, "Battery Mode", Kind.BAT), - Integer("warning_code", 170, "Warning code"), # 35185 - Integer("safety_country", 172, "Safety Country code", "", Kind.AC), # 35186 - Enum2("safety_country_label", 172, SAFETY_COUNTRIES, "Safety Country", Kind.AC), - Integer("work_mode", 174, "Work Mode code"), # 35187 - Enum2("work_mode_label", 174, WORK_MODES_ET, "Work Mode"), - Integer("operation_mode", 176, "Operation Mode code"), # 35188 ? - Long("error_codes", 178, "Error Codes"), - EnumBitmap4("errors", 178, ERROR_CODES, "Errors"), # 35189 - Energy4("e_total", 182, "Total PV Generation", Kind.PV), # 35190/91 - Energy4("e_day", 186, "Today's PV Generation", Kind.PV), # 35192/93 - Energy4("e_total_exp", 190, "Total Energy (export)", Kind.AC), # 35194/95 - Long("h_total", 194, "Hours Total", "h", Kind.PV), # 35196/97 - Energy("e_day_exp", 198, "Today Energy (export)", Kind.AC), - Energy4("e_total_imp", 200, "Total Energy (import)", Kind.AC), - Energy("e_day_imp", 204, "Today Energy (import)", Kind.AC), - Energy4("e_load_total", 206, "Total Load", Kind.AC), - Energy("e_load_day", 210, "Today Load", Kind.AC), - Energy4("e_bat_charge_total", 212, "Total Battery Charge", Kind.BAT), - Energy("e_bat_charge_day", 216, "Today Battery Charge", Kind.BAT), - Energy4("e_bat_discharge_total", 218, "Total Battery Discharge", Kind.BAT), - Energy("e_bat_discharge_day", 222, "Today Battery Discharge", Kind.BAT), - Long("diagnose_result", 240, "Diag Status Code"), - EnumBitmap4("diagnose_result_label", 240, DIAG_STATUS_CODES, "Diag Status"), + lambda data: round(read_voltage(data, 35180) * read_current(data, 35181)), + "Battery Power", "W", Kind.BAT), + # 35182+35183 ? + Integer("battery_mode", 35184, "Battery Mode code", "", Kind.BAT), + Enum2("battery_mode_label", 35184, BATTERY_MODES, "Battery Mode", Kind.BAT), + Integer("warning_code", 35185, "Warning code"), + Integer("safety_country", 35186, "Safety Country code", "", Kind.AC), + Enum2("safety_country_label", 35186, SAFETY_COUNTRIES, "Safety Country", Kind.AC), + Integer("work_mode", 35187, "Work Mode code"), + Enum2("work_mode_label", 35187, WORK_MODES_ET, "Work Mode"), + Integer("operation_mode", 35188, "Operation Mode code"), + Long("error_codes", 35189, "Error Codes"), + EnumBitmap4("errors", 35189, ERROR_CODES, "Errors"), + Energy4("e_total", 35191, "Total PV Generation", Kind.PV), + Energy4("e_day", 35193, "Today's PV Generation", Kind.PV), + Energy4("e_total_exp", 35195, "Total Energy (export)", Kind.AC), + Long("h_total", 35197, "Hours Total", "h", Kind.PV), + Energy("e_day_exp", 35199, "Today Energy (export)", Kind.AC), + Energy4("e_total_imp", 35200, "Total Energy (import)", Kind.AC), + Energy("e_day_imp", 35202, "Today Energy (import)", Kind.AC), + Energy4("e_load_total", 35203, "Total Load", Kind.AC), + Energy("e_load_day", 35205, "Today Load", Kind.AC), + Energy4("e_bat_charge_total", 35206, "Total Battery Charge", Kind.BAT), + Energy("e_bat_charge_day", 35208, "Today Battery Charge", Kind.BAT), + Energy4("e_bat_discharge_total", 35209, "Total Battery Discharge", Kind.BAT), + Energy("e_bat_discharge_day", 35211, "Today Battery Discharge", Kind.BAT), + Long("diagnose_result", 35220, "Diag Status Code"), + EnumBitmap4("diagnose_result_label", 35220, DIAG_STATUS_CODES, "Diag Status"), # ppv1 + ppv2 + pbattery - active_power Calculated("house_consumption", lambda data: - read_bytes4(data, 10) + - read_bytes4(data, 18) + - read_bytes4(data, 26) + - read_bytes4(data, 34) + - round(read_voltage(data, 160) * read_current(data, 162)) - - read_bytes2(data, 80), + read_bytes4(data, 35105) + + read_bytes4(data, 35109) + + read_bytes4(data, 35113) + + read_bytes4(data, 35117) + + round(read_voltage(data, 35180) * read_current(data, 35181)) - + read_bytes2(data, 35140), "House Consumption", "W", Kind.AC), ) # Modbus registers from offset 0x9088 (37000) __all_sensors_battery: Tuple[Sensor, ...] = ( - Integer("battery_bms", 0, "Battery BMS", "", Kind.BAT), # 37000 - Integer("battery_index", 2, "Battery Index", "", Kind.BAT), # 37001 - Integer("battery_status", 4, "Battery Status", "", Kind.BAT), # 37002 - Temp("battery_temperature", 6, "Battery Temperature", Kind.BAT), # 37003 - Integer("battery_charge_limit", 8, "Battery Charge Limit", "A", Kind.BAT), # 37004 - Integer("battery_discharge_limit", 10, "Battery Discharge Limit", "A", Kind.BAT), # 37005 - Integer("battery_error_l", 12, "Battery Error L", "", Kind.BAT), # 37006 - Integer("battery_soc", 14, "Battery State of Charge", "%", Kind.BAT), # 37007 - Integer("battery_soh", 16, "Battery State of Health", "%", Kind.BAT), # 37008 - Integer("battery_modules", 18, "Battery Modules", "", Kind.BAT), # 37009 - Integer("battery_warning_l", 20, "Battery Warning L", "", Kind.BAT), # 37010 - Integer("battery_protocol", 22, "Battery Protocol", "", Kind.BAT), # 37011 - Integer("battery_error_h", 24, "Battery Error H", "", Kind.BAT), # 37012 - EnumBitmap22("battery_error", 24, 12, BMS_ALARM_CODES, "Battery Error", Kind.BAT), - Integer("battery_warning_h", 26, "Battery Warning H", "", Kind.BAT), # 37013 - EnumBitmap22("battery_warning", 26, 20, BMS_WARNING_CODES, "Battery Warning", Kind.BAT), - Integer("battery_sw_version", 28, "Battery Software Version", "", Kind.BAT), # 37014 - Integer("battery_hw_version", 30, "Battery Hardware Version", "", Kind.BAT), # 37015 - Integer("battery_max_cell_temp_id", 32, "Battery Max Cell Temperature ID", "", Kind.BAT), # 37016 - Integer("battery_min_cell_temp_id", 34, "Battery Min Cell Temperature ID", "", Kind.BAT), # 37017 - Integer("battery_max_cell_voltage_id", 36, "Battery Max Cell Voltage ID", "", Kind.BAT), # 37018 - Integer("battery_min_cell_voltage_id", 38, "Battery Min Cell Voltage ID", "", Kind.BAT), # 37019 - Temp("battery_max_cell_temp", 40, "Battery Max Cell Temperature", Kind.BAT), # 37020 - Temp("battery_min_cell_temp", 42, "Battery Min Cell Temperature", Kind.BAT), # 37021 - Voltage("battery_max_cell_voltage", 44, "Battery Max Cell Voltage", Kind.BAT), # 37022 - Voltage("battery_min_cell_voltage", 46, "Battery Min Cell Voltage", Kind.BAT), # 37023 - # Energy4("battery_total_charge", 112, "Total Battery 1 Charge", Kind.BAT), #37056 - # Energy4("battery_total_discharge", 116, "Total Battery 1 Discharge", Kind.BAT), # 37058 - # String8("battery_sn", 120, "Battery S/N", Kind.BAT), # 37060-67 + Integer("battery_bms", 37000, "Battery BMS", "", Kind.BAT), + Integer("battery_index", 37001, "Battery Index", "", Kind.BAT), + Integer("battery_status", 37002, "Battery Status", "", Kind.BAT), + Temp("battery_temperature", 37003, "Battery Temperature", Kind.BAT), + Integer("battery_charge_limit", 37004, "Battery Charge Limit", "A", Kind.BAT), + Integer("battery_discharge_limit", 37005, "Battery Discharge Limit", "A", Kind.BAT), + Integer("battery_error_l", 37006, "Battery Error L", "", Kind.BAT), + Integer("battery_soc", 37007, "Battery State of Charge", "%", Kind.BAT), + Integer("battery_soh", 37008, "Battery State of Health", "%", Kind.BAT), + Integer("battery_modules", 37009, "Battery Modules", "", Kind.BAT), + Integer("battery_warning_l", 37010, "Battery Warning L", "", Kind.BAT), + Integer("battery_protocol", 37011, "Battery Protocol", "", Kind.BAT), + Integer("battery_error_h", 37012, "Battery Error H", "", Kind.BAT), + EnumBitmap22("battery_error", 37012, 37006, BMS_ALARM_CODES, "Battery Error", Kind.BAT), + Integer("battery_warning_h", 37013, "Battery Warning H", "", Kind.BAT), + EnumBitmap22("battery_warning", 37013, 37010, BMS_WARNING_CODES, "Battery Warning", Kind.BAT), + Integer("battery_sw_version", 37014, "Battery Software Version", "", Kind.BAT), + Integer("battery_hw_version", 37015, "Battery Hardware Version", "", Kind.BAT), + Integer("battery_max_cell_temp_id", 37016, "Battery Max Cell Temperature ID", "", Kind.BAT), + Integer("battery_min_cell_temp_id", 37017, "Battery Min Cell Temperature ID", "", Kind.BAT), + Integer("battery_max_cell_voltage_id", 37018, "Battery Max Cell Voltage ID", "", Kind.BAT), + Integer("battery_min_cell_voltage_id", 37019, "Battery Min Cell Voltage ID", "", Kind.BAT), + Temp("battery_max_cell_temp", 37020, "Battery Max Cell Temperature", Kind.BAT), + Temp("battery_min_cell_temp", 37021, "Battery Min Cell Temperature", Kind.BAT), + Voltage("battery_max_cell_voltage", 37022, "Battery Max Cell Voltage", Kind.BAT), + Voltage("battery_min_cell_voltage", 37023, "Battery Min Cell Voltage", Kind.BAT), + # Energy4("battery_total_charge", 37056, "Total Battery 1 Charge", Kind.BAT), + # Energy4("battery_total_discharge", 37058, "Total Battery 1 Discharge", Kind.BAT), + # String8("battery_sn", 37060, "Battery S/N", Kind.BAT), ) # Modbus registers from offset 0x9858 (39000) __all_sensors_battery2: Tuple[Sensor, ...] = ( - Integer("battery2_status", 0, "Battery 2 Status", "", Kind.BAT), # 39000 - Temp("battery2_temperature", 2, "Battery 2 Temperature", Kind.BAT), # 39001 - Integer("battery2_charge_limit", 4, "Battery 2 Charge Limit", "A", Kind.BAT), # 39002 - Integer("battery2_discharge_limit", 6, "Battery 2 Discharge Limit", "A", Kind.BAT), # 39003 - Integer("battery2_error_l", 8, "Battery 2 rror L", "", Kind.BAT), # 39004 - Integer("battery2_soc", 10, "Battery 2 State of Charge", "%", Kind.BAT), # 39005 - Integer("battery2_soh", 12, "Battery 2 State of Health", "%", Kind.BAT), # 39006 - Integer("battery2_modules", 14, "Battery 2 Modules", "", Kind.BAT), # 39007 - Integer("battery2_warning_l", 16, "Battery 2 Warning L", "", Kind.BAT), # 39008 - Integer("battery2_protocol", 18, "Battery 2 Protocol", "", Kind.BAT), # 39009 - Integer("battery2_error_h", 20, "Battery 2 Error H", "", Kind.BAT), # 39010 - EnumBitmap22("battery2_error", 20, 8, BMS_ALARM_CODES, "Battery 2 Error", Kind.BAT), - Integer("battery2_warning_h", 22, "Battery 2 Warning H", "", Kind.BAT), # 39011 - EnumBitmap22("battery2_warning", 22, 16, BMS_WARNING_CODES, "Battery 2 Warning", Kind.BAT), - Integer("battery2_sw_version", 24, "Battery 2 Software Version", "", Kind.BAT), # 39012 - Integer("battery2_hw_version", 26, "Battery 2 Hardware Version", "", Kind.BAT), # 39013 - Integer("battery2_max_cell_temp_id", 28, "Battery 2 Max Cell Temperature ID", "", Kind.BAT), # 39014 - Integer("battery2_min_cell_temp_id", 30, "Battery 2 Min Cell Temperature ID", "", Kind.BAT), # 39015 - Integer("battery2_max_cell_voltage_id", 32, "Battery 2 Max Cell Voltage ID", "", Kind.BAT), # 39016 - Integer("battery2_min_cell_voltage_id", 34, "Battery 2 Min Cell Voltage ID", "", Kind.BAT), # 39017 - Temp("battery2_max_cell_temp", 36, "Battery 2 Max Cell Temperature", Kind.BAT), # 39018 - Temp("battery2_min_cell_temp", 38, "Battery 2 Min Cell Temperature", Kind.BAT), # 39019 - Voltage("battery2_max_cell_voltage", 40, "Battery 2 Max Cell Voltage", Kind.BAT), # 39020 - Voltage("battery2_min_cell_voltage", 42, "Battery 2 Min Cell Voltage", Kind.BAT), # 39021 - # Energy4("battery2_total_charge", 108, "Total Battery 2 Charge", Kind.BAT), #39054 - # Energy4("battery2_total_discharge", 112, "Total Battery 2 Discharge", Kind.BAT), # 39056 - # String8("battery2_sn", 120, "Battery 2 S/N", Kind.BAT), # 39058-65 + Integer("battery2_status", 39000, "Battery 2 Status", "", Kind.BAT), + Temp("battery2_temperature", 39001, "Battery 2 Temperature", Kind.BAT), + Integer("battery2_charge_limit", 39002, "Battery 2 Charge Limit", "A", Kind.BAT), + Integer("battery2_discharge_limit", 39003, "Battery 2 Discharge Limit", "A", Kind.BAT), + Integer("battery2_error_l", 39004, "Battery 2 rror L", "", Kind.BAT), + Integer("battery2_soc", 39005, "Battery 2 State of Charge", "%", Kind.BAT), + Integer("battery2_soh", 39006, "Battery 2 State of Health", "%", Kind.BAT), + Integer("battery2_modules", 39007, "Battery 2 Modules", "", Kind.BAT), + Integer("battery2_warning_l", 39008, "Battery 2 Warning L", "", Kind.BAT), + Integer("battery2_protocol", 39009, "Battery 2 Protocol", "", Kind.BAT), + Integer("battery2_error_h", 39010, "Battery 2 Error H", "", Kind.BAT), + EnumBitmap22("battery2_error", 39010, 39004, BMS_ALARM_CODES, "Battery 2 Error", Kind.BAT), + Integer("battery2_warning_h", 39011, "Battery 2 Warning H", "", Kind.BAT), + EnumBitmap22("battery2_warning", 39011, 39008, BMS_WARNING_CODES, "Battery 2 Warning", Kind.BAT), + Integer("battery2_sw_version", 39012, "Battery 2 Software Version", "", Kind.BAT), + Integer("battery2_hw_version", 39013, "Battery 2 Hardware Version", "", Kind.BAT), + Integer("battery2_max_cell_temp_id", 39014, "Battery 2 Max Cell Temperature ID", "", Kind.BAT), + Integer("battery2_min_cell_temp_id", 39015, "Battery 2 Min Cell Temperature ID", "", Kind.BAT), + Integer("battery2_max_cell_voltage_id", 39016, "Battery 2 Max Cell Voltage ID", "", Kind.BAT), + Integer("battery2_min_cell_voltage_id", 39017, "Battery 2 Min Cell Voltage ID", "", Kind.BAT), + Temp("battery2_max_cell_temp", 39018, "Battery 2 Max Cell Temperature", Kind.BAT), + Temp("battery2_min_cell_temp", 39019, "Battery 2 Min Cell Temperature", Kind.BAT), + Voltage("battery2_max_cell_voltage", 39020, "Battery 2 Max Cell Voltage", Kind.BAT), + Voltage("battery2_min_cell_voltage", 39021, "Battery 2 Min Cell Voltage", Kind.BAT), + # Energy4("battery2_total_charge", 39054, "Total Battery 2 Charge", Kind.BAT), + # Energy4("battery2_total_discharge", 39056, "Total Battery 2 Discharge", Kind.BAT), + # String8("battery2_sn", 39058, "Battery 2 S/N", Kind.BAT), ) # Inverter's meter data # Modbus registers from offset 0x8ca0 (36000) __all_sensors_meter: Tuple[Sensor, ...] = ( - Integer("commode", 0, "Commode"), # 36000 - Integer("rssi", 2, "RSSI"), # 36001 - Integer("manufacture_code", 4, "Manufacture Code"), # 36002 - Integer("meter_test_status", 6, "Meter Test Status"), # 1: correct,2: reverse,3: incorrect,0: not checked - Integer("meter_comm_status", 8, "Meter Communication Status"), # 36004 # 1 OK, 0 NotOK - Power("active_power1", 10, "Active Power L1", Kind.GRID), # 36005 - Power("active_power2", 12, "Active Power L2", Kind.GRID), # 36006 - Power("active_power3", 14, "Active Power L3", Kind.GRID), # 36007 - Power("active_power_total", 16, "Active Power Total", Kind.GRID), # 36008 - Reactive("reactive_power_total", 18, "Reactive Power Total", Kind.GRID), # 36009 - Decimal("meter_power_factor1", 20, 1000, "Meter Power Factor L1", "", Kind.GRID), # 36010 - Decimal("meter_power_factor2", 22, 1000, "Meter Power Factor L2", "", Kind.GRID), # 36011 - Decimal("meter_power_factor3", 24, 1000, "Meter Power Factor L3", "", Kind.GRID), # 36012 - Decimal("meter_power_factor", 26, 1000, "Meter Power Factor", "", Kind.GRID), # 36013 - Frequency("meter_freq", 28, "Meter Frequency", Kind.GRID), # 36014 - Float("meter_e_total_exp", 30, 1000, "Meter Total Energy (export)", "kWh", Kind.GRID), # 36015/16 - Float("meter_e_total_imp", 34, 1000, "Meter Total Energy (import)", "kWh", Kind.GRID), # 36017/18 - Power4("meter_active_power1", 38, "Meter Active Power L1", Kind.GRID), # 36019/20 - Power4("meter_active_power2", 42, "Meter Active Power L2", Kind.GRID), # 36021/22 - Power4("meter_active_power3", 46, "Meter Active Power L3", Kind.GRID), # 36023/24 - Power4("meter_active_power_total", 50, "Meter Active Power Total", Kind.GRID), # 36025/26 - Reactive4("meter_reactive_power1", 54, "Meter Reactive Power L1", Kind.GRID), # 36027/28 - Reactive4("meter_reactive_power2", 58, "Meter Reactive Power L2", Kind.GRID), # 36029/30 - Reactive4("meter_reactive_power3", 62, "Meter Reactive Power L2", Kind.GRID), # 36031/32 - Reactive4("meter_reactive_power_total", 66, "Meter Reactive Power Total", Kind.GRID), # 36033/34 - Apparent4("meter_apparent_power1", 70, "Meter Apparent Power L1", Kind.GRID), # 36035/36 - Apparent4("meter_apparent_power2", 74, "Meter Apparent Power L2", Kind.GRID), # 36037/38 - Apparent4("meter_apparent_power3", 78, "Meter Apparent Power L3", Kind.GRID), # 36039/40 - Apparent4("meter_apparent_power_total", 82, "Meter Apparent Power Total", Kind.GRID), # 36041/42 - Integer("meter_type", 86, "Meter Type", "", Kind.GRID), # 36043 (0: Single phase, 1: 3P3W, 2: 3P4W, 3: HomeKit) - Integer("meter_sw_version", 88, "Meter Software Version", "", Kind.GRID), # 36044 + Integer("commode", 36000, "Commode"), + Integer("rssi", 36001, "RSSI"), + Integer("manufacture_code", 36002, "Manufacture Code"), + Integer("meter_test_status", 36003, "Meter Test Status"), # 1: correct,2: reverse,3: incorrect,0: not checked + Integer("meter_comm_status", 36004, "Meter Communication Status"), # 1 OK, 0 NotOK + Power("active_power1", 36005, "Active Power L1", Kind.GRID), + Power("active_power2", 36006, "Active Power L2", Kind.GRID), + Power("active_power3", 36007, "Active Power L3", Kind.GRID), + Power("active_power_total", 36008, "Active Power Total", Kind.GRID), + Reactive("reactive_power_total", 36009, "Reactive Power Total", Kind.GRID), + Decimal("meter_power_factor1", 36010, 1000, "Meter Power Factor L1", "", Kind.GRID), + Decimal("meter_power_factor2", 36011, 1000, "Meter Power Factor L2", "", Kind.GRID), + Decimal("meter_power_factor3", 36012, 1000, "Meter Power Factor L3", "", Kind.GRID), + Decimal("meter_power_factor", 36013, 1000, "Meter Power Factor", "", Kind.GRID), + Frequency("meter_freq", 36014, "Meter Frequency", Kind.GRID), + Float("meter_e_total_exp", 36015, 1000, "Meter Total Energy (export)", "kWh", Kind.GRID), + Float("meter_e_total_imp", 36017, 1000, "Meter Total Energy (import)", "kWh", Kind.GRID), + Power4("meter_active_power1", 36019, "Meter Active Power L1", Kind.GRID), + Power4("meter_active_power2", 36021, "Meter Active Power L2", Kind.GRID), + Power4("meter_active_power3", 36023, "Meter Active Power L3", Kind.GRID), + Power4("meter_active_power_total", 36025, "Meter Active Power Total", Kind.GRID), + Reactive4("meter_reactive_power1", 36027, "Meter Reactive Power L1", Kind.GRID), + Reactive4("meter_reactive_power2", 36029, "Meter Reactive Power L2", Kind.GRID), + Reactive4("meter_reactive_power3", 36031, "Meter Reactive Power L2", Kind.GRID), + Reactive4("meter_reactive_power_total", 36033, "Meter Reactive Power Total", Kind.GRID), + Apparent4("meter_apparent_power1", 36035, "Meter Apparent Power L1", Kind.GRID), + Apparent4("meter_apparent_power2", 36037, "Meter Apparent Power L2", Kind.GRID), + Apparent4("meter_apparent_power3", 36039, "Meter Apparent Power L3", Kind.GRID), + Apparent4("meter_apparent_power_total", 36041, "Meter Apparent Power Total", Kind.GRID), + Integer("meter_type", 36043, "Meter Type", "", Kind.GRID), # (0: Single phase, 1: 3P3W, 2: 3P4W, 3: HomeKit) + Integer("meter_sw_version", 36044, "Meter Software Version", "", Kind.GRID), # Sensors added in some ARM fw update - Power4("meter2_active_power", 90, "Meter 2 Active Power", Kind.GRID), # 36045/46 - Float("meter2_e_total_exp", 94, 1000, "Meter 2 Total Energy (export)", "kWh", Kind.GRID), # 36047/48 - Float("meter2_e_total_imp", 98, 1000, "Meter 2 Total Energy (import)", "kWh", Kind.GRID), # 36049/50 - Integer("meter2_comm_status", 102, "Meter 2 Communication Status"), # 36051 - Voltage("meter_voltage1", 104, "Meter L1 Voltage", Kind.GRID), # 36052 - Voltage("meter_voltage2", 106, "Meter L2 Voltage", Kind.GRID), # 36053 - Voltage("meter_voltage3", 108, "Meter L3 Voltage", Kind.GRID), # 36054 - Current("meter_current1", 110, "Meter L1 Current", Kind.GRID), # 36055 - Current("meter_current2", 112, "Meter L2 Current", Kind.GRID), # 36056 - Current("meter_current3", 114, "Meter L3 Current", Kind.GRID), # 36057 + Power4("meter2_active_power", 36045, "Meter 2 Active Power", Kind.GRID), + Float("meter2_e_total_exp", 36047, 1000, "Meter 2 Total Energy (export)", "kWh", Kind.GRID), + Float("meter2_e_total_imp", 36049, 1000, "Meter 2 Total Energy (import)", "kWh", Kind.GRID), + Integer("meter2_comm_status", 36051, "Meter 2 Communication Status"), + Voltage("meter_voltage1", 36052, "Meter L1 Voltage", Kind.GRID), + Voltage("meter_voltage2", 36053, "Meter L2 Voltage", Kind.GRID), + Voltage("meter_voltage3", 36054, "Meter L3 Voltage", Kind.GRID), + Current("meter_current1", 36055, "Meter L1 Current", Kind.GRID), + Current("meter_current2", 36056, "Meter L2 Current", Kind.GRID), + Current("meter_current3", 36057, "Meter L3 Current", Kind.GRID), ) # Inverter's MPPT data # Modbus registers from offset 0x89e5 (35301) __all_sensors_mptt: Tuple[Sensor, ...] = ( - Power4("ppv_total", 0, "PV Power Total", Kind.PV), # 35301 + Power4("ppv_total", 35301, "PV Power Total", Kind.PV), # 35303 PV channel RO U16 1 1 PV channel - Voltage("vpv5", 6, "PV5 Voltage", Kind.PV), # 35304 - Current("ipv5", 8, "PV5 Current", Kind.PV), # 35305 - Voltage("vpv6", 10, "PV6 Voltage", Kind.PV), # 35306 - Current("ipv6", 12, "PV6 Current", Kind.PV), # 35307 - Voltage("vpv7", 14, "PV7 Voltage", Kind.PV), # 35308 - Current("ipv7", 16, "PV7 Current", Kind.PV), # 35309 - Voltage("vpv8", 18, "PV8 Voltage", Kind.PV), # 35310 - Current("ipv8", 20, "PV8 Current", Kind.PV), # 35311 - Voltage("vpv9", 22, "PV9 Voltage", Kind.PV), # 35312 - Current("ipv9", 24, "PV9 Current", Kind.PV), # 35313 - Voltage("vpv10", 26, "PV10 Voltage", Kind.PV), # 35314 - Current("ipv10", 28, "PV10 Current", Kind.PV), # 35315 - Voltage("vpv11", 30, "PV11 Voltage", Kind.PV), # 35316 - Current("ipv11", 32, "PV11 Current", Kind.PV), # 35317 - Voltage("vpv12", 34, "PV12 Voltage", Kind.PV), # 35318 - Current("ipv12", 36, "PV12 Current", Kind.PV), # 35319 - Voltage("vpv13", 38, "PV13 Voltage", Kind.PV), # 35320 - Current("ipv13", 40, "PV13 Current", Kind.PV), # 35321 - Voltage("vpv14", 42, "PV14 Voltage", Kind.PV), # 35322 - Current("ipv14", 44, "PV14 Current", Kind.PV), # 35323 - Voltage("vpv15", 46, "PV15 Voltage", Kind.PV), # 35324 - Current("ipv15", 48, "PV15 Current", Kind.PV), # 35325 - Voltage("vpv16", 50, "PV16 Voltage", Kind.PV), # 35326 - Current("ipv16", 52, "PV16 Current", Kind.PV), # 35327 + Voltage("vpv5", 35304, "PV5 Voltage", Kind.PV), + Current("ipv5", 35305, "PV5 Current", Kind.PV), + Voltage("vpv6", 35306, "PV6 Voltage", Kind.PV), + Current("ipv6", 35307, "PV6 Current", Kind.PV), + Voltage("vpv7", 35308, "PV7 Voltage", Kind.PV), + Current("ipv7", 35309, "PV7 Current", Kind.PV), + Voltage("vpv8", 35310, "PV8 Voltage", Kind.PV), + Current("ipv8", 35311, "PV8 Current", Kind.PV), + Voltage("vpv9", 35312, "PV9 Voltage", Kind.PV), + Current("ipv9", 35313, "PV9 Current", Kind.PV), + Voltage("vpv10", 35314, "PV10 Voltage", Kind.PV), + Current("ipv10", 35315, "PV10 Current", Kind.PV), + Voltage("vpv11", 35316, "PV11 Voltage", Kind.PV), + Current("ipv11", 35317, "PV11 Current", Kind.PV), + Voltage("vpv12", 35318, "PV12 Voltage", Kind.PV), + Current("ipv12", 35319, "PV12 Current", Kind.PV), + Voltage("vpv13", 35320, "PV13 Voltage", Kind.PV), + Current("ipv13", 35321, "PV13 Current", Kind.PV), + Voltage("vpv14", 35322, "PV14 Voltage", Kind.PV), + Current("ipv14", 35323, "PV14 Current", Kind.PV), + Voltage("vpv15", 35324, "PV15 Voltage", Kind.PV), + Current("ipv15", 35325, "PV15 Current", Kind.PV), + Voltage("vpv16", 35326, "PV16 Voltage", Kind.PV), + Current("ipv16", 35327, "PV16 Current", Kind.PV), # 35328 Warning Message # 35330 Grid10minAvgVoltR # 35331 Grid10minAvgVoltS # 35332 Grid10minAvgVoltT # 35333 Error Message Extend # 35335 Warning Message Extend - Power("pmppt1", 72, "MPPT1 Power", Kind.PV), # 35337 - Power("pmppt2", 74, "MPPT2 Power", Kind.PV), # 35338 - Power("pmppt3", 76, "MPPT3 Power", Kind.PV), # 35339 - Power("pmppt4", 78, "MPPT4 Power", Kind.PV), # 35340 - Power("pmppt5", 80, "MPPT5 Power", Kind.PV), # 35341 - Power("pmppt6", 82, "MPPT6 Power", Kind.PV), # 35342 - Power("pmppt7", 84, "MPPT7 Power", Kind.PV), # 35343 - Power("pmppt8", 86, "MPPT8 Power", Kind.PV), # 35344 - Power("imppt1", 88, "MPPT1 Current", Kind.PV), # 35345 - Power("imppt2", 90, "MPPT2 Current", Kind.PV), # 35346 - Power("imppt3", 92, "MPPT3 Current", Kind.PV), # 35347 - Power("imppt4", 94, "MPPT4 Current", Kind.PV), # 35348 - Power("imppt5", 96, "MPPT5 Current", Kind.PV), # 35349 - Power("imppt6", 98, "MPPT6 Current", Kind.PV), # 35350 - Power("imppt7", 100, "MPPT7 Current", Kind.PV), # 35351 - Power("imppt8", 102, "MPPT8 Current", Kind.PV), # 35352 - Reactive4("reactive_power1", 104, "Reactive Power L1", Kind.GRID), # 35353/54 - Reactive4("reactive_power2", 108, "Reactive Power L2", Kind.GRID), # 35355/56 - Reactive4("reactive_power3", 112, "Reactive Power L3", Kind.GRID), # 35357/58 - Apparent4("apparent_power1", 116, "Apparent Power L1", Kind.GRID), # 35359/60 - Apparent4("apparent_power2", 120, "Apparent Power L2", Kind.GRID), # 35361/62 - Apparent4("apparent_power3", 124, "Apparent Power L3", Kind.GRID), # 35363/64 + Power("pmppt1", 35337, "MPPT1 Power", Kind.PV), + Power("pmppt2", 35338, "MPPT2 Power", Kind.PV), + Power("pmppt3", 35339, "MPPT3 Power", Kind.PV), + Power("pmppt4", 35340, "MPPT4 Power", Kind.PV), + Power("pmppt5", 35341, "MPPT5 Power", Kind.PV), + Power("pmppt6", 35342, "MPPT6 Power", Kind.PV), + Power("pmppt7", 35343, "MPPT7 Power", Kind.PV), + Power("pmppt8", 35344, "MPPT8 Power", Kind.PV), + Power("imppt1", 35345, "MPPT1 Current", Kind.PV), + Power("imppt2", 35346, "MPPT2 Current", Kind.PV), + Power("imppt3", 35347, "MPPT3 Current", Kind.PV), + Power("imppt4", 35348, "MPPT4 Current", Kind.PV), + Power("imppt5", 35349, "MPPT5 Current", Kind.PV), + Power("imppt6", 35350, "MPPT6 Current", Kind.PV), + Power("imppt7", 35351, "MPPT7 Current", Kind.PV), + Power("imppt8", 35352, "MPPT8 Current", Kind.PV), + Reactive4("reactive_power1", 35353, "Reactive Power L1", Kind.GRID), + Reactive4("reactive_power2", 35355, "Reactive Power L2", Kind.GRID), + Reactive4("reactive_power3", 35357, "Reactive Power L3", Kind.GRID), + Apparent4("apparent_power1", 35359, "Apparent Power L1", Kind.GRID), + Apparent4("apparent_power2", 35361, "Apparent Power L2", Kind.GRID), + Apparent4("apparent_power3", 35363, "Apparent Power L3", Kind.GRID), ) # Modbus registers of inverter settings, offsets are modbus register addresses @@ -439,7 +440,7 @@ def _single_phase_only(s: Sensor) -> bool: @staticmethod def _not_extended_meter(s: Sensor) -> bool: """Filter to exclude extended meter sensors""" - return s.offset < 90 + return s.offset < 36045 async def read_device_info(self): response = await self._read_from_socket(self._READ_DEVICE_VERSION_INFO) @@ -548,8 +549,7 @@ async def read_setting(self, setting_id: str) -> Any: raise ValueError(f'Unknown setting "{setting_id}"') count = (setting.size_ + (setting.size_ % 2)) // 2 response = await self._read_from_socket(ModbusReadCommand(self.comm_addr, setting.offset, count)) - with io.BytesIO(response.response_data()) as buffer: - return setting.read_value(buffer) + return setting.read_value(response) async def write_setting(self, setting_id: str, value: Any): setting = self._settings.get(setting_id) diff --git a/goodwe/inverter.py b/goodwe/inverter.py index 4925978..c880e21 100644 --- a/goodwe/inverter.py +++ b/goodwe/inverter.py @@ -1,7 +1,6 @@ from __future__ import annotations import asyncio -import io import logging from abc import ABC, abstractmethod from dataclasses import dataclass @@ -44,11 +43,11 @@ class Sensor: unit: str kind: Optional[SensorKind] - def read_value(self, data: io.BytesIO) -> Any: + def read_value(self, data: ProtocolResponse) -> Any: """Read the sensor value from data at current position""" raise NotImplementedError() - def read(self, data: io.BytesIO) -> Any: + def read(self, data: ProtocolResponse) -> Any: """Read the sensor value from data (at sensor offset)""" data.seek(self.offset) return self.read_value(data) @@ -283,16 +282,15 @@ def settings(self) -> Tuple[Sensor, ...]: @staticmethod def _map_response(response: ProtocolResponse, sensors: Tuple[Sensor, ...], incl_xx: bool = True) -> Dict[str, Any]: """Process the response data and return dictionary with runtime values""" - with io.BytesIO(response.response_data()) as buffer: - result = {} - for sensor in sensors: - if incl_xx or not sensor.id_.startswith("xx"): - try: - result[sensor.id_] = sensor.read(buffer) - except ValueError: - logger.exception("Error reading sensor %s.", sensor.id_) - result[sensor.id_] = None - return result + result = {} + for sensor in sensors: + if incl_xx or not sensor.id_.startswith("xx"): + try: + result[sensor.id_] = sensor.read(response) + except ValueError: + logger.exception("Error reading sensor %s.", sensor.id_) + result[sensor.id_] = None + return result @staticmethod def _decode(data: bytes) -> str: diff --git a/goodwe/protocol.py b/goodwe/protocol.py index bb11e65..c5950c3 100644 --- a/goodwe/protocol.py +++ b/goodwe/protocol.py @@ -1,6 +1,7 @@ from __future__ import annotations import asyncio +import io import logging from asyncio.futures import Future from typing import Tuple, Optional, Callable @@ -87,12 +88,25 @@ class ProtocolResponse: def __init__(self, raw_data: bytes, command: ProtocolCommand): self.raw_data: bytes = raw_data self.command: ProtocolCommand = command + self._bytes: io.BytesIO = io.BytesIO(self.response_data()) def __repr__(self): return self.raw_data.hex() def response_data(self) -> bytes: - return self.command.trim_response(self.raw_data) + if self.command is not None: + return self.command.trim_response(self.raw_data) + else: + return self.raw_data + + def seek(self, address: int) -> None: + if self.command is not None: + self._bytes.seek(self.command.get_offset(address)) + else: + self._bytes.seek(address) + + def read(self, size: int) -> bytes: + return self._bytes.read(size) class ProtocolCommand: @@ -109,6 +123,10 @@ def trim_response(self, raw_response: bytes): """Trim raw response from header and checksum data""" return raw_response + def get_offset(self, address: int): + """Calculate relative offset to start of the response bytes""" + return address + async def execute(self, host: str, timeout: int, retries: int) -> ProtocolResponse: """ Execute the udp protocol command on the specified address/port. @@ -257,11 +275,16 @@ def __init__(self, request: bytes, cmd: int, offset: int, value: int): request, lambda x: validate_modbus_response(x, cmd, offset, value), ) + self.first_address: int = offset def trim_response(self, raw_response: bytes): """Trim raw response from header and checksum data""" return raw_response[5:-2] + def get_offset(self, address: int): + """Calculate relative offset to start of the response bytes""" + return (address - self.first_address) * 2 + class ModbusReadCommand(ModbusProtocolCommand): """ diff --git a/goodwe/sensor.py b/goodwe/sensor.py index b9fe1db..a3993fa 100644 --- a/goodwe/sensor.py +++ b/goodwe/sensor.py @@ -1,6 +1,5 @@ from __future__ import annotations -import io from abc import ABC, abstractmethod from datetime import datetime from struct import unpack @@ -8,6 +7,7 @@ from .const import * from .inverter import Sensor, SensorKind +from .protocol import ProtocolResponse DAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] @@ -18,7 +18,7 @@ class Voltage(Sensor): def __init__(self, id_: str, offset: int, name: str, kind: Optional[SensorKind]): super().__init__(id_, offset, name, 2, "V", kind) - def read_value(self, data: io.BytesIO): + def read_value(self, data: ProtocolResponse): return read_voltage(data) def encode_value(self, value: Any) -> bytes: @@ -31,7 +31,7 @@ class Current(Sensor): def __init__(self, id_: str, offset: int, name: str, kind: Optional[SensorKind]): super().__init__(id_, offset, name, 2, "A", kind) - def read_value(self, data: io.BytesIO): + def read_value(self, data: ProtocolResponse): return read_current(data) def encode_value(self, value: Any) -> bytes: @@ -44,7 +44,7 @@ class Frequency(Sensor): def __init__(self, id_: str, offset: int, name: str, kind: Optional[SensorKind]): super().__init__(id_, offset, name, 2, "Hz", kind) - def read_value(self, data: io.BytesIO): + def read_value(self, data: ProtocolResponse): return read_freq(data) @@ -54,7 +54,7 @@ class Power(Sensor): def __init__(self, id_: str, offset: int, name: str, kind: Optional[SensorKind]): super().__init__(id_, offset, name, 2, "W", kind) - def read_value(self, data: io.BytesIO): + def read_value(self, data: ProtocolResponse): return read_bytes2(data) @@ -64,7 +64,7 @@ class Power4(Sensor): def __init__(self, id_: str, offset: int, name: str, kind: Optional[SensorKind]): super().__init__(id_, offset, name, 4, "W", kind) - def read_value(self, data: io.BytesIO): + def read_value(self, data: ProtocolResponse): return read_bytes4(data) @@ -74,7 +74,7 @@ class Energy(Sensor): def __init__(self, id_: str, offset: int, name: str, kind: Optional[SensorKind]): super().__init__(id_, offset, name, 2, "kWh", kind) - def read_value(self, data: io.BytesIO): + def read_value(self, data: ProtocolResponse): value = read_bytes2(data) if value == -1: return None @@ -88,7 +88,7 @@ class Energy4(Sensor): 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: io.BytesIO): + def read_value(self, data: ProtocolResponse): value = read_bytes4(data) if value == -1: return None @@ -102,7 +102,7 @@ class Apparent(Sensor): def __init__(self, id_: str, offset: int, name: str, kind: Optional[SensorKind]): super().__init__(id_, offset, name, 2, "VA", kind) - def read_value(self, data: io.BytesIO): + def read_value(self, data: ProtocolResponse): return read_bytes2(data) @@ -112,7 +112,7 @@ class Apparent4(Sensor): def __init__(self, id_: str, offset: int, name: str, kind: Optional[SensorKind]): super().__init__(id_, offset, name, 2, "VA", kind) - def read_value(self, data: io.BytesIO): + def read_value(self, data: ProtocolResponse): return read_bytes4(data) @@ -122,7 +122,7 @@ class Reactive(Sensor): def __init__(self, id_: str, offset: int, name: str, kind: Optional[SensorKind]): super().__init__(id_, offset, name, 2, "var", kind) - def read_value(self, data: io.BytesIO): + def read_value(self, data: ProtocolResponse): return read_bytes2(data) @@ -132,7 +132,7 @@ class Reactive4(Sensor): def __init__(self, id_: str, offset: int, name: str, kind: Optional[SensorKind]): super().__init__(id_, offset, name, 2, "var", kind) - def read_value(self, data: io.BytesIO): + def read_value(self, data: ProtocolResponse): return read_bytes4(data) @@ -142,7 +142,7 @@ class Temp(Sensor): def __init__(self, id_: str, offset: int, name: str, kind: Optional[SensorKind] = None): super().__init__(id_, offset, name, 2, "C", kind) - def read_value(self, data: io.BytesIO): + def read_value(self, data: ProtocolResponse): return read_temp(data) @@ -152,7 +152,7 @@ class Byte(Sensor): def __init__(self, id_: str, offset: int, name: str, unit: str = "", kind: Optional[SensorKind] = None): super().__init__(id_, offset, name, 1, unit, kind) - def read_value(self, data: io.BytesIO): + def read_value(self, data: ProtocolResponse): return read_byte(data) def encode_value(self, value: Any) -> bytes: @@ -165,6 +165,9 @@ class ByteH(Byte): def __init__(self, id_: str, offset: int, name: str, unit: str = "", kind: Optional[SensorKind] = None): super().__init__(id_, offset, name, unit, kind) + def read_value(self, data: ProtocolResponse): + return read_byte(data) + def encode_value(self, value: Any, register_value: bytes) -> bytes: word = bytearray(register_value) word[0] = int.to_bytes(int(value), length=1, byteorder="big", signed=True)[0] @@ -177,6 +180,10 @@ class ByteL(Byte): def __init__(self, id_: str, offset: int, name: str, unit: str = "", kind: Optional[SensorKind] = None): super().__init__(id_, offset, name, unit, kind) + def read_value(self, data: ProtocolResponse): + read_byte(data) + return read_byte(data) + def encode_value(self, value: Any, register_value: bytes) -> bytes: word = bytearray(register_value) word[1] = int.to_bytes(int(value), length=1, byteorder="big", signed=True)[0] @@ -189,7 +196,7 @@ class Integer(Sensor): def __init__(self, id_: str, offset: int, name: str, unit: str = "", kind: Optional[SensorKind] = None): super().__init__(id_, offset, name, 2, unit, kind) - def read_value(self, data: io.BytesIO): + def read_value(self, data: ProtocolResponse): return read_bytes2(data) def encode_value(self, value: Any) -> bytes: @@ -202,7 +209,7 @@ class Long(Sensor): def __init__(self, id_: str, offset: int, name: str, unit: str = "", kind: Optional[SensorKind] = None): super().__init__(id_, offset, name, 4, unit, kind) - def read_value(self, data: io.BytesIO): + def read_value(self, data: ProtocolResponse): return read_bytes4(data) def encode_value(self, value: Any) -> bytes: @@ -216,7 +223,7 @@ def __init__(self, id_: str, offset: int, scale: int, name: str, unit: str = "", super().__init__(id_, offset, name, 2, unit, kind) self.scale = scale - def read_value(self, data: io.BytesIO): + def read_value(self, data: ProtocolResponse): return read_decimal2(data, self.scale) def encode_value(self, value: Any) -> bytes: @@ -230,7 +237,7 @@ def __init__(self, id_: str, offset: int, scale: int, name: str, unit: str = "", super().__init__(id_, offset, name, 4, unit, kind) self.scale = scale - def read_value(self, data: io.BytesIO): + def read_value(self, data: ProtocolResponse): return round(read_float4(data) / self.scale, 3) @@ -240,7 +247,7 @@ class Timestamp(Sensor): def __init__(self, id_: str, offset: int, name: str, kind: Optional[SensorKind] = None): super().__init__(id_, offset, name, 6, "", kind) - def read_value(self, data: io.BytesIO): + def read_value(self, data: ProtocolResponse): return read_datetime(data) def encode_value(self, value: Any) -> bytes: @@ -254,7 +261,30 @@ def __init__(self, id_: str, offset: int, labels: Dict, name: str, kind: Optiona super().__init__(id_, offset, name, 1, "", kind) self._labels: Dict = labels - def read_value(self, data: io.BytesIO): + def read_value(self, data: ProtocolResponse): + return self._labels.get(read_byte(data)) + + +class EnumH(Sensor): + """Sensor representing label from enumeration encoded in 1 (high 8 bits of 16bit register)""" + + def __init__(self, id_: str, offset: int, labels: Dict, name: str, kind: Optional[SensorKind] = None): + super().__init__(id_, offset, name, 1, "", kind) + self._labels: Dict = labels + + def read_value(self, data: ProtocolResponse): + return self._labels.get(read_byte(data)) + + +class EnumL(Sensor): + """Sensor representing label from enumeration encoded in 1 bytes (low 8 bits of 16bit register)""" + + def __init__(self, id_: str, offset: int, labels: Dict, name: str, kind: Optional[SensorKind] = None): + super().__init__(id_, offset, name, 1, "", kind) + self._labels: Dict = labels + + def read_value(self, data: ProtocolResponse): + read_byte(data) return self._labels.get(read_byte(data)) @@ -265,7 +295,7 @@ def __init__(self, id_: str, offset: int, labels: Dict, name: str, kind: Optiona super().__init__(id_, offset, name, 2, "", kind) self._labels: Dict = labels - def read_value(self, data: io.BytesIO): + def read_value(self, data: ProtocolResponse): return self._labels.get(read_bytes2(data)) @@ -276,10 +306,10 @@ def __init__(self, id_: str, offset: int, labels: Dict, name: str, kind: Optiona super().__init__(id_, offset, name, 4, "", kind) self._labels: Dict = labels - def read_value(self, data: io.BytesIO) -> Any: + def read_value(self, data: ProtocolResponse) -> Any: raise NotImplementedError() - def read(self, data: io.BytesIO): + def read(self, data: ProtocolResponse): return decode_bitmap(read_bytes4(data, self.offset), self._labels) @@ -292,26 +322,26 @@ def __init__(self, id_: str, offsetH: int, offsetL: int, labels: Dict, name: str self._labels: Dict = labels self._offsetL: int = offsetL - def read_value(self, data: io.BytesIO) -> Any: + def read_value(self, data: ProtocolResponse) -> Any: raise NotImplementedError() - def read(self, data: io.BytesIO): + def read(self, data: ProtocolResponse): return decode_bitmap(read_bytes2(data, self.offset) << 16 + read_bytes2(data, self._offsetL), self._labels) class EnumCalculated(Sensor): """Sensor representing label from enumeration of calculated value""" - def __init__(self, id_: str, getter: Callable[[io.BytesIO], Any], labels: Dict, name: str, + def __init__(self, id_: str, getter: Callable[[ProtocolResponse], Any], labels: Dict, name: str, kind: Optional[SensorKind] = None): super().__init__(id_, 0, name, 0, "", kind) - self._getter: Callable[[io.BytesIO], Any] = getter + self._getter: Callable[[ProtocolResponse], Any] = getter self._labels: Dict = labels - def read_value(self, data: io.BytesIO) -> Any: + def read_value(self, data: ProtocolResponse) -> Any: raise NotImplementedError() - def read(self, data: io.BytesIO): + def read(self, data: ProtocolResponse): return self._labels.get(self._getter(data)) @@ -357,7 +387,7 @@ def __init__(self, id_: str, offset: int, name: str): def __str__(self): return f"{self.start_h}:{self.start_m}-{self.end_h}:{self.end_m} {self.days} {self.power}% {'On' if self.on_off != 0 else 'Off'}" - def read_value(self, data: io.BytesIO): + def read_value(self, data: ProtocolResponse): self.start_h = read_byte(data) if (self.start_h < 0 or self.start_h > 23) and self.start_h != 48: raise ValueError(f"{self.id_}: start_h value {self.start_h} out of range.") @@ -385,7 +415,7 @@ def read_value(self, data: io.BytesIO): def encode_value(self, value: Any) -> bytes: if isinstance(value, bytes) and len(value) == 8: # try to read_value to check if values are valid - if self.read_value(io.BytesIO(value)): + if self.read_value(ProtocolResponse(value, None)): return value raise ValueError @@ -455,7 +485,7 @@ def __init__(self, id_: str, offset: int, name: str): def __str__(self): return f"{self.start_h}:{self.start_m}-{self.end_h}:{self.end_m} {self.days} {self.power}% (SoC {self.soc}%) {'On' if self.on_off != 0 else 'Off'}" - def read_value(self, data: io.BytesIO): + def read_value(self, data: ProtocolResponse): self.start_h = read_byte(data) if (self.start_h < 0 or self.start_h > 23) and self.start_h != 48: raise ValueError(f"{self.id_}: start_h value {self.start_h} out of range.") @@ -486,7 +516,7 @@ def read_value(self, data: io.BytesIO): def encode_value(self, value: Any) -> bytes: if isinstance(value, bytes) and len(value) == 12: # try to read_value to check if values are valid - if self.read_value(io.BytesIO(value)): + if self.read_value(ProtocolResponse(value, None)): return value raise ValueError @@ -556,7 +586,7 @@ def __init__(self, id_: str, offset: int, name: str): def __str__(self): return f"{self.start_h}:{self.start_m}-{self.end_h}:{self.end_m} {self.days} {self.import_power}kW (SoC {self.soc}%) {'On' if self.on_off == -4 else 'Off'}" - def read_value(self, data: io.BytesIO): + def read_value(self, data: ProtocolResponse): self.start_h = read_byte(data) if (self.start_h < 0 or self.start_h > 23) and self.start_h != 48: raise ValueError(f"{self.id_}: start_h value {self.start_h} out of range.") @@ -587,7 +617,7 @@ def read_value(self, data: io.BytesIO): def encode_value(self, value: Any) -> bytes: if isinstance(value, bytes) and len(value) == 12: # try to read_value to check if values are valid - if self.read_value(io.BytesIO(value)): + if self.read_value(ProtocolResponse(value, None)): return value raise ValueError @@ -599,47 +629,47 @@ def encode_off(self) -> bytes: class Calculated(Sensor): """Sensor representing calculated value""" - def __init__(self, id_: str, getter: Callable[[io.BytesIO], Any], name: str, unit: str, + def __init__(self, id_: str, getter: Callable[[ProtocolResponse], Any], name: str, unit: str, kind: Optional[SensorKind] = None): super().__init__(id_, 0, name, 0, unit, kind) - self._getter: Callable[[io.BytesIO], Any] = getter + self._getter: Callable[[ProtocolResponse], Any] = getter - def read_value(self, data: io.BytesIO) -> Any: + def read_value(self, data: ProtocolResponse) -> Any: raise NotImplementedError() - def read(self, data: io.BytesIO): + def read(self, data: ProtocolResponse): return self._getter(data) -def read_byte(buffer: io.BytesIO, offset: int = None) -> int: +def read_byte(buffer: ProtocolResponse, offset: int = None) -> int: """Retrieve single byte (signed int) value from buffer""" if offset is not None: buffer.seek(offset) return int.from_bytes(buffer.read(1), byteorder="big", signed=True) -def read_bytes2(buffer: io.BytesIO, offset: int = None) -> int: +def read_bytes2(buffer: ProtocolResponse, offset: int = None) -> int: """Retrieve 2 byte (signed int) value from buffer""" if offset is not None: buffer.seek(offset) return int.from_bytes(buffer.read(2), byteorder="big", signed=True) -def read_bytes4(buffer: io.BytesIO, offset: int = None) -> int: +def read_bytes4(buffer: ProtocolResponse, offset: int = None) -> int: """Retrieve 4 byte (signed int) value from buffer""" if offset is not None: buffer.seek(offset) return int.from_bytes(buffer.read(4), byteorder="big", signed=True) -def read_decimal2(buffer: io.BytesIO, scale: int, offset: int = None) -> float: +def read_decimal2(buffer: ProtocolResponse, scale: int, offset: int = None) -> float: """Retrieve 2 byte (signed float) value from buffer""" if offset is not None: buffer.seek(offset) return float(int.from_bytes(buffer.read(2), byteorder="big", signed=True)) / scale -def read_float4(buffer: io.BytesIO, offset: int = None) -> float: +def read_float4(buffer: ProtocolResponse, offset: int = None) -> float: """Retrieve 4 byte (signed float) value from buffer""" if offset is not None: buffer.seek(offset) @@ -650,7 +680,7 @@ def read_float4(buffer: io.BytesIO, offset: int = None) -> float: return float(0) -def read_voltage(buffer: io.BytesIO, offset: int = None) -> float: +def read_voltage(buffer: ProtocolResponse, offset: int = None) -> float: """Retrieve voltage [V] value (2 bytes) from buffer""" if offset is not None: buffer.seek(offset) @@ -663,7 +693,7 @@ def encode_voltage(value: Any) -> bytes: return int.to_bytes(int(value * 10), length=2, byteorder="big", signed=True) -def read_current(buffer: io.BytesIO, offset: int = None) -> float: +def read_current(buffer: ProtocolResponse, offset: int = None) -> float: """Retrieve current [A] value (2 bytes) from buffer""" if offset is not None: buffer.seek(offset) @@ -676,7 +706,7 @@ def encode_current(value: Any) -> bytes: return int.to_bytes(int(value * 10), length=2, byteorder="big", signed=True) -def read_freq(buffer: io.BytesIO, offset: int = None) -> float: +def read_freq(buffer: ProtocolResponse, offset: int = None) -> float: """Retrieve frequency [Hz] value (2 bytes) from buffer""" if offset is not None: buffer.seek(offset) @@ -684,7 +714,7 @@ def read_freq(buffer: io.BytesIO, offset: int = None) -> float: return float(value) / 100 -def read_temp(buffer: io.BytesIO, offset: int = None) -> float: +def read_temp(buffer: ProtocolResponse, offset: int = None) -> float: """Retrieve temperature [C] value (2 bytes) from buffer""" if offset is not None: buffer.seek(offset) @@ -692,7 +722,7 @@ def read_temp(buffer: io.BytesIO, offset: int = None) -> float: return float(value) / 10 -def read_datetime(buffer: io.BytesIO, offset: int = None) -> datetime: +def read_datetime(buffer: ProtocolResponse, offset: int = None) -> datetime: """Retrieve datetime value (6 bytes) from buffer""" if offset is not None: buffer.seek(offset) @@ -722,7 +752,7 @@ def encode_datetime(value: Any) -> bytes: return result -def read_grid_mode(buffer: io.BytesIO, offset: int = None) -> int: +def read_grid_mode(buffer: ProtocolResponse, offset: int = None) -> int: """Retrieve 'grid mode' sign value from buffer""" value = read_bytes2(buffer, offset) if value < -90: diff --git a/tests/test_sensor.py b/tests/test_sensor.py index b6d2b4b..175fe1e 100644 --- a/tests/test_sensor.py +++ b/tests/test_sensor.py @@ -3,20 +3,32 @@ from goodwe.sensor import * +class MockResponse(ProtocolResponse): + + def __init__(self, response: str): + super().__init__(bytes.fromhex(response), None) + + def response_data(self) -> bytes: + return self.raw_data + + class TestUtils(TestCase): def test_byte(self): testee = Byte("", 0, "", "", None) - data = io.BytesIO(bytes.fromhex("0c")) + data = MockResponse("0c") self.assertEqual(12, testee.read(data)) - data = io.BytesIO(bytes.fromhex("f0")) + data = MockResponse("f0") self.assertEqual(-16, testee.read(data)) def test_byteH(self): testee = ByteH("", 0, "", "", None) + data = MockResponse("2039") + self.assertEqual(32, testee.read(data)) + self.assertEqual("2039", testee.encode_value(32, bytes.fromhex("3039")).hex()) self.assertEqual("ff39", testee.encode_value(-1, bytes.fromhex("3039")).hex()) self.assertEqual("7f39", testee.encode_value(127, bytes.fromhex("3039")).hex()) @@ -25,6 +37,9 @@ def test_byteH(self): def test_byteL(self): testee = ByteL("", 0, "", "", None) + data = MockResponse("307f") + self.assertEqual(127, testee.read(data)) + self.assertEqual("3020", testee.encode_value(32, bytes.fromhex("3039")).hex()) self.assertEqual("30ff", testee.encode_value(-1, bytes.fromhex("3039")).hex()) self.assertEqual("307f", testee.encode_value(127, bytes.fromhex("3039")).hex()) @@ -33,74 +48,74 @@ def test_byteL(self): def test_integer(self): testee = Integer("", 0, "", "", None) - data = io.BytesIO(bytes.fromhex("0031")) + data = MockResponse("0031") self.assertEqual(49, testee.read(data)) self.assertEqual("0031", testee.encode_value(49).hex()) - data = io.BytesIO(bytes.fromhex("ff9e")) + data = MockResponse("ff9e") self.assertEqual(-98, testee.read(data)) self.assertEqual("ff9e", testee.encode_value(-98).hex()) def test_decimal(self): testee = Decimal("", 0, 10, "", "", None) - data = io.BytesIO(bytes.fromhex("0031")) + data = MockResponse("0031") self.assertEqual(4.9, testee.read(data)) self.assertEqual("0031", testee.encode_value(4.9).hex()) - data = io.BytesIO(bytes.fromhex("ff9e")) + data = MockResponse("ff9e") self.assertEqual(-9.8, testee.read(data)) self.assertEqual("ff9e", testee.encode_value(-9.8).hex()) def test_voltage(self): testee = Voltage("", 0, "", None) - data = io.BytesIO(bytes.fromhex("0cfe")) + data = MockResponse("0cfe") self.assertEqual(332.6, testee.read(data)) self.assertEqual("0cfe", testee.encode_value(332.6).hex()) - data = io.BytesIO(bytes.fromhex("1f64")) + data = MockResponse("1f64") self.assertEqual(803.6, testee.read(data)) self.assertEqual("1f64", testee.encode_value(803.6).hex()) def test_current(self): testee = Current("", 0, "", None) - data = io.BytesIO(bytes.fromhex("0031")) + data = MockResponse("0031") self.assertEqual(4.9, testee.read(data)) self.assertEqual("0031", testee.encode_value(4.9).hex()) - data = io.BytesIO(bytes.fromhex("ff9e")) + data = MockResponse("ff9e") self.assertEqual(-9.8, testee.read(data)) self.assertEqual("ff9e", testee.encode_value(-9.8).hex()) def test_power4(self): testee = Power4("", 0, "", None) - data = io.BytesIO(bytes.fromhex("0000069f")) + data = MockResponse("0000069f") self.assertEqual(1695, testee.read(data)) - data = io.BytesIO(bytes.fromhex("fffffffd")) + data = MockResponse("fffffffd") self.assertEqual(-3, testee.read(data)) def test_energy(self): testee = Energy("", 0, "", None) - data = io.BytesIO(bytes.fromhex("0972")) + data = MockResponse("0972") self.assertEqual(241.8, testee.read(data)) def test_energy4(self): testee = Energy4("", 0, "", None) - data = io.BytesIO(bytes.fromhex("00020972")) + data = MockResponse("00020972") self.assertEqual(13349.0, testee.read(data)) - data = io.BytesIO(bytes.fromhex("ffffffff")) + data = MockResponse("ffffffff") self.assertIsNone(testee.read(data)) def test_timestamp(self): testee = Timestamp("", 0, "", None) - data = io.BytesIO(bytes.fromhex("160104121e19")) + data = MockResponse("160104121e19") self.assertEqual(datetime(2022, 1, 4, 18, 30, 25), testee.read(data)) self.assertEqual("160104121e19", testee.encode_value(datetime(2022, 1, 4, 18, 30, 25)).hex()) self.assertEqual("160104121e19", testee.encode_value("2022-01-04T18:30:25").hex()) @@ -108,7 +123,7 @@ def test_timestamp(self): def test_eco_mode_v1(self): testee = EcoModeV1("", 0, "") - data = io.BytesIO(bytes.fromhex("0d1e0e28ffc4ff1a")) + data = MockResponse("0d1e0e28ffc4ff1a") self.assertEqual("13:30-14:40 Mon,Wed,Thu -60% On", testee.read(data).__str__()) self.assertEqual(bytes.fromhex("0d1e0e28ffc4ff1a"), testee.encode_value(bytes.fromhex("0d1e0e28ffc4ff1a"))) self.assertRaises(ValueError, lambda: testee.encode_value(bytes.fromhex("0d1e0e28ffc4ffff"))) @@ -116,19 +131,19 @@ def test_eco_mode_v1(self): self.assertFalse(testee.read(data).is_eco_charge_mode()) self.assertFalse(testee.read(data).is_eco_discharge_mode()) - data = io.BytesIO(testee.encode_charge(-40)) + data = MockResponse(testee.encode_charge(-40).hex()) self.assertEqual("0:0-23:59 Sun,Mon,Tue,Wed,Thu,Fri,Sat -40% On", testee.read(data).__str__()) self.assertTrue(testee.read(data).is_eco_charge_mode()) self.assertFalse(testee.read(data).is_eco_discharge_mode()) self.assertEqual("0:0-23:59 Sun,Mon,Tue,Wed,Thu,Fri,Sat -40% (SoC 100%) On", testee.as_eco_mode_v2().__str__()) - data = io.BytesIO(testee.encode_discharge(60)) + data = MockResponse(testee.encode_discharge(60).hex()) self.assertEqual("0:0-23:59 Sun,Mon,Tue,Wed,Thu,Fri,Sat 60% On", testee.read(data).__str__()) self.assertFalse(testee.read(data).is_eco_charge_mode()) self.assertTrue(testee.read(data).is_eco_discharge_mode()) self.assertEqual("0:0-23:59 Sun,Mon,Tue,Wed,Thu,Fri,Sat 60% (SoC 100%) On", testee.as_eco_mode_v2().__str__()) - data = io.BytesIO(testee.encode_off()) + data = MockResponse(testee.encode_off().hex()) self.assertEqual("48:0-48:0 100% Off", testee.read(data).__str__()) self.assertFalse(testee.read(data).is_eco_charge_mode()) self.assertFalse(testee.read(data).is_eco_discharge_mode()) @@ -136,7 +151,7 @@ def test_eco_mode_v1(self): def test_eco_mode_v2(self): testee = EcoModeV2("", 0, "") - data = io.BytesIO(bytes.fromhex("0d1e0e28ff1affc4005a0000")) + data = MockResponse("0d1e0e28ff1affc4005a0000") self.assertEqual("13:30-14:40 Mon,Wed,Thu -60% (SoC 90%) On", testee.read(data).__str__()) self.assertEqual(bytes.fromhex("0d1e0e28ff1affc4005a0000"), testee.encode_value(bytes.fromhex("0d1e0e28ff1affc4005a0000"))) @@ -145,17 +160,17 @@ def test_eco_mode_v2(self): self.assertFalse(testee.read(data).is_eco_charge_mode()) self.assertFalse(testee.read(data).is_eco_discharge_mode()) - data = io.BytesIO(testee.encode_charge(-40, 80)) + data = MockResponse(testee.encode_charge(-40, 80).hex()) self.assertEqual("0:0-23:59 Sun,Mon,Tue,Wed,Thu,Fri,Sat -40% (SoC 80%) On", testee.read(data).__str__()) self.assertTrue(testee.read(data).is_eco_charge_mode()) self.assertFalse(testee.read(data).is_eco_discharge_mode()) self.assertEqual("0:0-23:59 Sun,Mon,Tue,Wed,Thu,Fri,Sat -40% On", testee.as_eco_mode_v1().__str__()) - data = io.BytesIO(testee.encode_discharge(60)) + data = MockResponse(testee.encode_discharge(60).hex()) self.assertEqual("0:0-23:59 Sun,Mon,Tue,Wed,Thu,Fri,Sat 60% (SoC 100%) On", testee.read(data).__str__()) self.assertFalse(testee.read(data).is_eco_charge_mode()) self.assertTrue(testee.read(data).is_eco_discharge_mode()) self.assertEqual("0:0-23:59 Sun,Mon,Tue,Wed,Thu,Fri,Sat 60% On", testee.as_eco_mode_v1().__str__()) - data = io.BytesIO(testee.encode_off()) + data = MockResponse(testee.encode_off().hex()) self.assertEqual("48:0-48:0 100% (SoC 100%) Off", testee.read(data).__str__()) self.assertFalse(testee.read(data).is_eco_charge_mode()) self.assertFalse(testee.read(data).is_eco_discharge_mode()) @@ -163,7 +178,7 @@ def test_eco_mode_v2(self): def test_peak_shaving_mode(self): testee = PeakShavingMode("", 0, "") - data = io.BytesIO(bytes.fromhex("010a020a037f00fa00370000")) + data = MockResponse("010a020a037f00fa00370000") self.assertEqual("1:10-2:10 Sun,Mon,Tue,Wed,Thu,Fri,Sat 2.5kW (SoC 55%) Off", testee.read(data).__str__()) self.assertEqual(bytes.fromhex("010a020a037f00fa00370000"), testee.encode_value(bytes.fromhex("010a020a037f00fa00370000")))