Files
shaggy-solar/LVX6048/powermon-patches/pi18.py
2026-04-27 06:50:04 -04:00

708 lines
40 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
""" powermon / protocols / pi18.py """
import logging
from powermon.commands.command import CommandType
from powermon.commands.command_definition import CommandDefinition
from powermon.commands.reading_definition import ReadingType, ResponseType
from powermon.commands.result import ResultType
from powermon.libs.errors import CommandDefinitionMissing, InvalidCRC, InvalidResponse
from powermon.ports import PortType
from powermon.protocols.abstractprotocol import AbstractProtocol
from powermon.protocols.helpers import crc_pi30 as crc
from powermon.protocols.pi30 import BATTERY_TYPE_LIST, OUTPUT_MODE_LIST
log = logging.getLogger("pi18")
SETTER_COMMANDS = {
"POP": {
"name": "POP",
"command_type": CommandType.PI18_SETTER,
"description": "Set Device Output Source Priority",
"help": " -- examples: POP0 (set utility first), POP01 (set solar first)",
"regex": "POP([01])$",
},
"PSP": {
"name": "PSP",
"command_type": CommandType.PI18_SETTER,
"description": "Set Solar Power priority",
"help": " -- examples: PSP0 (Battery-Load-Utility +AC Charge), PSP1 (Load-Battery-Utility)",
"regex": "PSP([01])$",
},
"PEI": {
"name": "PEI",
"command_type": CommandType.PI18_SETTER,
"description": "Set Machine type, enable: Grid-Tie",
"help": " -- examples: PEI (enable Grid-Tie)",
},
"PDI": {
"name": "PDI",
"command_type": CommandType.PI18_SETTER,
"description": "Set Machine type, disable: Grid-Tie",
"help": " -- examples: PDI (disable Grid-Tie)",
},
"PCP": {
"name": "PCP",
"command_type": CommandType.PI18_SETTER,
"description": "Set Device Charger Priority",
"help": " -- examples: PCP0,1 (set unit 0 [0-9] to Solar and Utility) PCP0,0 (set unit 0 to Solar first), PCP0,1 (set unit 0 to Solar and Utility), PCP0,2 (set unit 0 to solar only charging)",
"regex": "PCP([0-9],[012])$",
},
"MCHGC": {
"name": "MCHGC",
"command_type": CommandType.PI18_SETTER,
"description": "Set Battery Max Charging Current Solar + AC",
"help": " -- examples: MCHGC0,040 (set unit 0 to max charging current of 40A), MCHGC1,060 (set unit 1 to max charging current of 60A) [010 020 030 040 050 060 070 080]",
"regex": "MCHGC([0-9],0[1-8]0)$",
},
"MUCHGC": {
"name": "MUCHGC",
"command_type": CommandType.PI18_SETTER,
"description": "Set Battery Max AC Charging Current",
"help": " -- examples: MUCHGC0,040 (set unit 0 to max charging current of 40A), MUCHGC1,060 (set unit 1 to max charging current of 60A) [002 010 020 030 040 050 060 070 080]",
"regex": "MUCHGC([0-9]),(002|0[1-8]0)$",
},
"PBT": {
"name": "PBT",
"command_type": CommandType.PI18_SETTER,
"description": "Set Battery Type",
"help": " -- examples: PBT0 (set battery as AGM), PBT1 (set battery as FLOODED), PBT2 (set battery as USER)",
"regex": "PBT([012])$",
},
"MCHGV": {
"name": "MCHGV",
"command_type": CommandType.PI18_SETTER,
"description": "Set Battery Bulk,Float Charging Voltages",
"help": " -- example MCHGV552,540 - set battery charging voltage Bulk to 52.2V, float 54V (set Bulk Voltage [480~584] in 0.1V xxx, Float Voltage [480~584] in 0.1V yyy)",
# Regex 48.0 - 58.4 Volt
"regex": "MCHGV(4[8-9][0-9]|5[0-7][0-9]|58[0-5]),(4[8-9][0-9]|5[0-7][0-9]|58[0-4])$",
},
"PSDV": {
"name": "PSDV",
"command_type": CommandType.PI18_SETTER,
"description": "Set Battery Cut-off Voltage",
"help": " -- example PSDV400 - set battery cut-off voltage to 40V [400~480V] for 48V unit)",
# Regex 40 to 48V
"regex": "PSDV(4[0-7][0-9]|480)$",
},
"BUCD": {
"name": "BUCD",
"command_type": CommandType.PI18_SETTER,
"description": "Set Battery Stop dis,charging when Grid is available",
"help": " -- example BUCD440,480 - set Stop discharge Voltage [440~510] in 0.1V xxx, Stop Charge Voltage [000(Full) or 480~580] in 0.1V yyy",
# Regex 44 to 51V, Full|48V to 58V
"regex": "BUCD((4[4-9]0|5[0-1]0),(000|4[8-9]0|5[0-8]0))$",
},
}
QUERY_COMMANDS = {
"PI": {
"name": "PI",
"command_type": CommandType.PI18_QUERY,
"description": "Protocol ID inquiry",
"help": " -- queries the protocol ID",
"result_type": ResultType.SINGLE,
"reading_definitions": [
{"description": "Protocol ID"},
],
"test_responses": [
b"^D00518m\xae\r"
]
},
"ID": {
"name": "ID",
"aliases": ["default", "get_id"],
"command_type": CommandType.PI18_QUERY,
"description": "Device Serial Number inquiry",
"help": " -- queries the device serial number",
"result_type": ResultType.SINGLE,
"reading_definitions": [{"description": "Serial Number"}],
"test_responses": [
b"^D02514012345678901234567\r",
],
},
"ET": {
"name": "ET",
"command_type": CommandType.PI18_QUERY,
"description": "Total PV Generated Energy Inquiry",
"result_type": ResultType.SINGLE,
"reading_definitions": [
{"description": "Total PV Generated Energy", "reading_type": ReadingType.WATT_HOURS,
"response_type": ResponseType.INT, "icon": "mdi:solar-power", "device_class": "energy", "state_class": "total"},
],
"test_responses": [
b""
],
},
"EY": {
"name": "EY",
"command_type": CommandType.PI18_QUERY,
"description": "Yearly PV Generated Energy Inquiry",
"result_type": ResultType.SINGLE,
"reading_definitions": [
{"description": "PV Generated Energy for Year", "reading_type": ReadingType.WATT_HOURS,
"response_type": ResponseType.INT, "icon": "mdi:counter", "device_class": "energy", "state_class": "total"},
{"description": "Year", "reading_type": ReadingType.YEAR,
"response_type": ResponseType.INFO_FROM_COMMAND, "format_template": "int(cn[3:])"},
],
"test_responses": [
b"^D01105580051\x0b\x9f\r",
],
"regex": "EY(\\d\\d\\d\\d)$",
},
"EM": {
"name": "EM",
"command_type": CommandType.PI18_QUERY,
"description": "Monthly PV Generated Energy Inquiry",
"result_type": ResultType.SINGLE,
"reading_definitions": [
{"description": "PV Generated Energy for Month", "reading_type": ReadingType.WATT_HOURS,
"response_type": ResponseType.INT, "icon": "mdi:solar-power", "device_class": "energy", "state_class": "total"},
{"description": "Year", "reading_type": ReadingType.YEAR,
"response_type": ResponseType.INFO_FROM_COMMAND, "format_template": "int(cn[3:7])"},
{"description": "Month", "reading_type": ReadingType.MONTH,
"response_type": ResponseType.INFO_FROM_COMMAND, "format_template": "calendar.month_name[int(cn[7:])]"},
],
"test_responses": [
b"",
],
"regex": "EM(\\d\\d\\d\\d\\d\\d)$",
},
"ED": {
"name": "ED",
"command_type": CommandType.PI18_QUERY,
"description": "Daily PV Generated Energy Inquiry",
"help": " -- display daily generated energy, format is QEDyyyymmdd",
"result_type": ResultType.SINGLE,
"reading_definitions": [
{"description": "PV Generated Energy for Day", "reading_type": ReadingType.WATT_HOURS,
"response_type": ResponseType.INT, "icon": "mdi:solar-power", "device_class": "energy", "state_class": "total"},
{"description": "Year", "reading_type": ReadingType.YEAR,
"response_type": ResponseType.INFO_FROM_COMMAND, "format_template": "int(cn[3:7])"},
{"description": "Month", "reading_type": ReadingType.MONTH,
"response_type": ResponseType.INFO_FROM_COMMAND, "format_template": "calendar.month_name[int(cn[7:9])]"},
{"description": "Day", "reading_type": ReadingType.DAY,
"response_type": ResponseType.INFO_FROM_COMMAND, "format_template": "int(cn[9:11])"},
],
"test_responses": [
b"(00238800!J\r",
],
"regex": "ED(\\d\\d\\d\\d\\d\\d\\d\\d)$",
},
"PIRI": {
"name": "PIRI",
"command_type": CommandType.PI18_QUERY,
"description": "Current Settings inquiry",
"help": " -- queries the current settings from the Inverter",
"result_type": ResultType.COMMA_DELIMITED,
"reading_definitions": [
{"description": "AC Input Voltage", "reading_type": ReadingType.VOLTS,
"response_type": ResponseType.TEMPLATE_INT, "format_template": "r/10"},
{"description": "AC Input Current", "reading_type": ReadingType.CURRENT,
"response_type": ResponseType.TEMPLATE_INT, "format_template": "r/10"},
{"description": "AC Output Voltage", "reading_type": ReadingType.VOLTS,
"response_type": ResponseType.TEMPLATE_INT, "format_template": "r/10"},
{"description": "AC Output Frequency", "reading_type": ReadingType.FREQUENCY,
"response_type": ResponseType.TEMPLATE_INT, "format_template": "r/10"},
{"description": "AC Output Current", "reading_type": ReadingType.CURRENT,
"response_type": ResponseType.TEMPLATE_INT, "format_template": "r/10"},
{"description": "AC Output Apparent Power", "reading_type": ReadingType.APPARENT_POWER},
{"description": "AC Output Active Power", "reading_type": ReadingType.WATTS},
{"description": "Battery Voltage", "reading_type": ReadingType.VOLTS,
"response_type": ResponseType.TEMPLATE_INT, "format_template": "r/10"},
{"description": "Battery re-charge Voltage", "reading_type": ReadingType.VOLTS,
"response_type": ResponseType.TEMPLATE_INT, "format_template": "r/10"},
{"description": "Battery re-discharge Voltage", "reading_type": ReadingType.VOLTS,
"response_type": ResponseType.TEMPLATE_INT, "format_template": "r/10"},
{"description": "Battery Under Voltage", "reading_type": ReadingType.VOLTS, "response_type": ResponseType.TEMPLATE_INT, "format_template": "r/10"},
{"description": "Battery Bulk Charge Voltage", "reading_type": ReadingType.VOLTS, "response_type": ResponseType.TEMPLATE_INT, "format_template": "r/10"},
{"description": "Battery Float Charge Voltage", "reading_type": ReadingType.VOLTS, "response_type": ResponseType.TEMPLATE_INT, "format_template": "r/10"},
{"description": "Battery Type", "reading_type": ReadingType.MESSAGE, "response_type": ResponseType.LIST, "options": BATTERY_TYPE_LIST},
{"description": "Max AC Charging Current", "reading_type": ReadingType.CURRENT},
{"description": "Max Charging Current", "reading_type": ReadingType.CURRENT},
{"description": "Input Voltage Range", "response_type": ResponseType.LIST, "options": ["Appliance", "UPS"]},
{"description": "Output Source Priority",
"response_type": ResponseType.LIST, "options": ["Solar - Utility - Battery", "Solar - Battery - Utility"]},
{"description": "Charger Source Priority",
"response_type": ResponseType.LIST, "options": ["Solar First", "Solar + Utility", "Only solar charging permitted"]},
{"description": "Max Parallel Units"},
{"description": "Machine Type", "response_type": ResponseType.LIST, "options": ["Off Grid", "Grid Tie"]},
{"description": "Topology", "response_type": ResponseType.LIST, "options": ["transformerless", "transformer"]},
{"description": "Output Mode", "reading_type": ReadingType.MESSAGE, "response_type": ResponseType.LIST, "options": OUTPUT_MODE_LIST},
{"description": "Solar power priority", "response_type": ResponseType.LIST, "options": ["Battery-Load-Utiliy + AC Charger", "Load-Battery-Utiliy"]},
{"description": "MPPT strings"},
{"description": "Unknown flags?", "response_type": ResponseType.STRING},
],
"test_responses": [
b"^D0882300,217,2300,500,217,5000,5000,480,480,530,440,570,570,2,10,070,1,1,1,9,0,0,0,0,1,00\xe1k\r",
]
},
"GS": {
"name": "GS",
"command_type": CommandType.PI18_QUERY,
"description": "General Status Parameters inquiry",
"result_type": ResultType.COMMA_DELIMITED,
"reading_definitions": [
{"description": "AC Input Voltage", "reading_type": ReadingType.VOLTS,
"response_type": ResponseType.TEMPLATE_INT, "format_template": "r/10", "icon": "mdi:transmission-tower-export", "device_class": "voltage"},
{"description": "AC Input Frequency", "reading_type": ReadingType.FREQUENCY,
"response_type": ResponseType.TEMPLATE_INT, "format_template": "r/10", "icon": "mdi:current-ac", "device_class": "frequency"},
{"description": "AC Output Voltage", "reading_type": ReadingType.VOLTS,
"response_type": ResponseType.TEMPLATE_INT, "format_template": "r/10", "icon": "mdi:transmission-tower-export", "device_class": "voltage"},
{"description": "AC Output Frequency", "reading_type": ReadingType.FREQUENCY,
"response_type": ResponseType.TEMPLATE_INT, "format_template": "r/10", "icon": "mdi:current-ac", "device_class": "frequency"},
{"description": "AC Output Apparent Power", "reading_type": ReadingType.APPARENT_POWER,
"response_type": ResponseType.INT, "icon": "mdi:power-plug", "device_class": "apparent_power"},
{"description": "AC Output Active Power", "reading_type": ReadingType.WATTS,
"response_type": ResponseType.INT, "icon": "mdi:power-plug", "device_class": "power", "state_class": "measurement"},
{"description": "AC Output Load", "reading_type": ReadingType.PERCENTAGE,
"response_type": ResponseType.INT, "icon": "mdi:brightness-percent"},
{"description": "Battery Voltage", "reading_type": ReadingType.VOLTS,
"response_type": ResponseType.TEMPLATE_INT, "format_template": "r/10", "icon": "mdi:battery-outline", "device_class": "voltage"},
{"description": "Battery Voltage from SCC", "reading_type": ReadingType.VOLTS,
"response_type": ResponseType.TEMPLATE_INT, "format_template": "r/10", "icon": "mdi:battery-outline", "device_class": "voltage"},
{"description": "Battery Voltage from SCC2", "reading_type": ReadingType.VOLTS,
"response_type": ResponseType.TEMPLATE_INT, "format_template": "r/10", "icon": "mdi:battery-outline", "device_class": "voltage"},
{"description": "Battery Discharge Current", "reading_type": ReadingType.CURRENT,
"response_type": ResponseType.INT, "icon": "mdi:battery-negative", "device_class": "current"},
{"description": "Battery Charging Current", "reading_type": ReadingType.CURRENT,
"response_type": ResponseType.INT, "icon": "mdi:current-dc", "device_class": "current"},
{"description": "Battery Capacity", "reading_type": ReadingType.PERCENTAGE,
"response_type": ResponseType.INT, "icon": "mdi:brightness-percent", "device_class": "battery"},
{"description": "Inverter heat sink temperature", "reading_type": ReadingType.TEMPERATURE,
"response_type": ResponseType.INT, "icon": "mdi:details", "device_class": "temperature"},
{"description": "MPPT1 charger temperature", "reading_type": ReadingType.TEMPERATURE,
"response_type": ResponseType.INT, "icon": "mdi:details", "device_class": "temperature"},
{"description": "MPPT2 charger temperature", "reading_type": ReadingType.TEMPERATURE,
"response_type": ResponseType.INT, "icon": "mdi:details", "device_class": "temperature"},
{"description": "MPPT1 Input Power", "reading_type": ReadingType.WATTS,
"response_type": ResponseType.INT, "icon": "mdi:solar-power", "device_class": "power", "state_class": "measurement"},
{"description": "MPPT2 Input Power", "reading_type": ReadingType.WATTS,
"response_type": ResponseType.INT, "icon": "mdi:solar-power", "device_class": "power", "state_class": "measurement"},
{"description": "MPPT1 Input Voltage", "reading_type": ReadingType.VOLTS,
"response_type": ResponseType.TEMPLATE_INT, "format_template": "r/10", "icon": "mdi:solar-power", "device_class": "voltage"},
{"description": "MPPT2 Input Voltage", "reading_type": ReadingType.VOLTS,
"response_type": ResponseType.TEMPLATE_INT, "format_template": "r/10", "icon": "mdi:solar-power", "device_class": "voltage"},
{"description": "Setting value configuration state", "reading_type": ReadingType.MESSAGE,
"response_type": ResponseType.OPTION,
"options": {
"0": "Nothing changed",
"1": "Something changed",
},
},
{"description": "MPPT1 charger status", "reading_type": ReadingType.MESSAGE,
"response_type": ResponseType.OPTION,
"options": {
"0": "abnormal",
"1": "normal but not charged",
"2": "charging",
},
},
{"description": "MPPT2 charger status", "reading_type": ReadingType.MESSAGE,
"response_type": ResponseType.OPTION,
"options": {
"0": "abnormal",
"1": "normal but not charged",
"2": "charging",
},
},
{"description": "Load connection", "reading_type": ReadingType.MESSAGE,
"response_type": ResponseType.OPTION,
"options": {
"0": "disconnect",
"1": "connect",
},
},
{"description": "Battery power direction", "reading_type": ReadingType.MESSAGE,
"response_type": ResponseType.OPTION,
"options": {
"0": "donothing",
"1": "charge",
"2": "discharge",
},
},
{"description": "DC AC power direction", "reading_type": ReadingType.MESSAGE,
"response_type": ResponseType.OPTION,
"options": {
"0": "donothing",
"1": "AC-DC",
"2": "DC-AC",
},
},
{"description": "Line power direction", "reading_type": ReadingType.MESSAGE,
"response_type": ResponseType.OPTION,
"options": {
"0": "donothing",
"1": "input",
"2": "output",
},
},
# Parallel-cluster index. PI18 spec: 0 = master, 1+ = slaves.
# Upstream powermon labels this as a 2-value flag ["Not valid", "valid"]
# which is wrong — masters end up labeled "Not valid". Fixed to expose
# the actual index. Range covers up to max_parallel_units (typ. 9).
{"description": "Parallel instance number", "reading_type": ReadingType.MESSAGE,
"response_type": ResponseType.OPTION,
"options": {
"0": "instance 0 (master)",
"1": "instance 1",
"2": "instance 2",
"3": "instance 3",
"4": "instance 4",
"5": "instance 5",
"6": "instance 6",
"7": "instance 7",
"8": "instance 8",
},
},
],
"test_responses": [
b"D1062232,499,2232,499,0971,0710,019,008,000,000,000,000,000,044,000,000,0520,0000,1941,0000,0,2,0,1,0,2,1,0\x09\x7b\r",
b"^D1062232,499,2232,499,1406,1376,028,549,000,000,000,010,095,060,000,000,0082,0000,1604,0000,0,2,0,1,1,1,1,0D\x12\r",
],
},
"MOD": {
"name": "MOD",
"command_type": CommandType.PI18_QUERY,
"description": "Mode inquiry",
"result_type": ResultType.SINGLE,
"reading_definitions": [
# Known modes 00..05 are from upstream's table. 06/07 observed
# post-commissioning while the inverter actively bank-charges and
# idles between solar cycles; "Charge" / "Eco" are educated guesses.
# Codes 08..15 are placeholders so an unexpected value doesn't crash
# the service (powermon's OPTION decoder raises KeyError on miss).
{"description": "Device Mode", "reading_type": ReadingType.MESSAGE,
"response_type": ResponseType.OPTION,
"options": {
"00": "Power on",
"01": "Standby",
"02": "Bypass",
"03": "Battery",
"04": "Fault",
"05": "Hybrid mode(Line mode, Grid mode)",
"06": "Charge",
"07": "Eco",
**{f"{i:02d}": f"Mode {i:02d}" for i in range(8, 16)},
}
},
],
"test_responses": [
b"^D00505\xd9\x9f\r",
],
},
"MCHGCR": {
"name": "MCHGCR",
"command_type": CommandType.PI18_QUERY,
"description": "Max Charging Current Options inquiry",
"help": " -- queries the maximum charging current setting of the Inverter",
"result_type": ResultType.MULTIVALUED,
"reading_definitions": [
{"description": "Max Charging Current Options", "reading_type": ReadingType.MESSAGE_AMPS,
"response_type": ResponseType.STRING
}
],
"test_responses": [
b"^D034010,020,030,040,050,060,070,080\x161\r",
],
},
"MUCHGCR": {
"name": "MUCHGCR",
"command_type": CommandType.PI18_QUERY,
"description": "Max Utility Charging Current Options inquiry",
"help": " -- queries the maximum utility charging current setting of the Inverter",
"result_type": ResultType.MULTIVALUED,
"reading_definitions": [
{"reading_type": ReadingType.MESSAGE_AMPS, "description": "Max Utility Charging Current", "response_type": ResponseType.STRING}
],
"test_responses": [
b"^D038002,010,020,030,040,050,060,070,080\xd01\r"
],
},
"FLAG": {
"name": "FLAG",
"command_type": CommandType.PI18_QUERY,
"description": "Query enable/disable flag status",
"result_type": ResultType.COMMA_DELIMITED,
"reading_definitions": [
{"description": "Buzzer beep", "reading_type": ReadingType.MESSAGE, "response_type": ResponseType.ENABLED_BOOL},
{"description": "Overload bypass function", "reading_type": ReadingType.MESSAGE, "response_type": ResponseType.ENABLED_BOOL},
{"description": "Display back to default page", "reading_type": ReadingType.MESSAGE, "response_type": ResponseType.ENABLED_BOOL},
{"description": "Overload restart", "reading_type": ReadingType.MESSAGE, "response_type": ResponseType.ENABLED_BOOL},
{"description": "Over temperature restart", "reading_type": ReadingType.MESSAGE, "response_type": ResponseType.ENABLED_BOOL},
{"description": "Backlight on", "reading_type": ReadingType.MESSAGE, "response_type": ResponseType.ENABLED_BOOL},
{"description": "Alarm primary source interrupt", "reading_type": ReadingType.MESSAGE, "response_type": ResponseType.ENABLED_BOOL},
{"description": "Fault code record", "reading_type": ReadingType.MESSAGE, "response_type": ResponseType.ENABLED_BOOL},
{"description": "Reserved", "reading_type": ReadingType.MESSAGE},
],
"test_responses": [
b"^D0200,0,0,0,0,1,0,0,12\xc2\x39\r",
],
},
"VFW": {
"name": "VFW",
"description": "Device CPU version inquiry",
"command_type": CommandType.PI18_QUERY,
"result_type": ResultType.COMMA_DELIMITED,
"reading_definitions": [
{"description": "Main CPU Version", "reading_type": ReadingType.MESSAGE},
{"description": "Slave 1 CPU Version", "reading_type": ReadingType.MESSAGE},
{"description": "Slave 2 CPU Version", "reading_type": ReadingType.MESSAGE},
],
"test_responses": [
b"^D02005220,00000,00000\x3e\xf8\r",
],
},
# Fault + warning bitmap. 2-digit fault code followed by ~32 0/1 warning bits.
# Fault-code list cross-referenced with PI30 QPGS (same firmware family).
"FWS": {
"name": "FWS",
"description": "Fault and warning status inquiry",
"command_type": CommandType.PI18_QUERY,
"result_type": ResultType.COMMA_DELIMITED,
"reading_definitions": [
{"description": "Fault code", "reading_type": ReadingType.MESSAGE,
"response_type": ResponseType.OPTION,
"options": {
"00": "No fault",
"01": "Fan is locked",
"02": "Over temperature",
"03": "Battery voltage is too high",
"04": "Battery voltage is too low",
"05": "Output short circuited or Over temperature",
"06": "Output voltage is too high",
"07": "Over load time out",
"08": "Bus voltage is too high",
"09": "Bus soft start failed",
"11": "Main relay failed",
"51": "Over current inverter",
"52": "Bus soft start failed",
"53": "Inverter soft start failed",
"54": "Self-test failed",
"55": "Over DC voltage on output of inverter",
"56": "Battery connection is open",
"57": "Current sensor failed",
"58": "Output voltage is too low",
"60": "Inverter negative power",
"71": "Parallel version different",
"72": "Output circuit failed",
"80": "CAN communication failed",
"81": "Parallel host line lost",
"82": "Parallel synchronized signal lost",
"83": "Parallel battery voltage detect different",
"84": "Parallel Line voltage or frequency detect different",
"85": "Parallel Line input current unbalanced",
"86": "Parallel output setting different",
}},
{"description": "PV loss warning", "response_type": ResponseType.ENABLED_BOOL},
{"description": "Inverter fault", "response_type": ResponseType.ENABLED_BOOL},
{"description": "Bus over", "response_type": ResponseType.ENABLED_BOOL},
{"description": "Bus under", "response_type": ResponseType.ENABLED_BOOL},
{"description": "Bus soft fail", "response_type": ResponseType.ENABLED_BOOL},
{"description": "Line fail", "response_type": ResponseType.ENABLED_BOOL},
{"description": "OPV short", "response_type": ResponseType.ENABLED_BOOL},
{"description": "Inverter voltage too low", "response_type": ResponseType.ENABLED_BOOL},
{"description": "Inverter voltage too high", "response_type": ResponseType.ENABLED_BOOL},
{"description": "Over temperature", "response_type": ResponseType.ENABLED_BOOL},
{"description": "Fan locked", "response_type": ResponseType.ENABLED_BOOL},
{"description": "Battery voltage high", "response_type": ResponseType.ENABLED_BOOL},
{"description": "Battery low alarm", "response_type": ResponseType.ENABLED_BOOL},
{"description": "Battery under shutdown", "response_type": ResponseType.ENABLED_BOOL},
{"description": "Battery derating", "response_type": ResponseType.ENABLED_BOOL},
{"description": "Overload", "response_type": ResponseType.ENABLED_BOOL},
{"description": "EEPROM fault", "response_type": ResponseType.ENABLED_BOOL},
{"description": "Inverter over current", "response_type": ResponseType.ENABLED_BOOL},
{"description": "Inverter soft fail", "response_type": ResponseType.ENABLED_BOOL},
{"description": "Self test fail", "response_type": ResponseType.ENABLED_BOOL},
{"description": "OP DC voltage over", "response_type": ResponseType.ENABLED_BOOL},
{"description": "Battery open", "response_type": ResponseType.ENABLED_BOOL},
{"description": "Current sensor fail", "response_type": ResponseType.ENABLED_BOOL},
{"description": "Battery short", "response_type": ResponseType.ENABLED_BOOL},
{"description": "Power limit", "response_type": ResponseType.ENABLED_BOOL},
{"description": "PV voltage high", "response_type": ResponseType.ENABLED_BOOL},
{"description": "MPPT overload fault", "response_type": ResponseType.ENABLED_BOOL},
{"description": "MPPT overload warning", "response_type": ResponseType.ENABLED_BOOL},
{"description": "Battery too low to charge", "response_type": ResponseType.ENABLED_BOOL},
{"description": "Battery weak", "response_type": ResponseType.ENABLED_BOOL},
{"description": "Battery equalization", "response_type": ResponseType.ENABLED_BOOL},
],
"test_responses": [
b"^D07100,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0\xaa\xaa\r",
],
},
# Per-unit parallel view. PGS<n>, n = 0..N-1 (0 is master).
# LVX6048 emits a 30-field layout that differs from the PI30 QPGS layout;
# only fields confirmed against live responses are semantically named here.
# The rest are exposed as raw strings so the command doesn't error out, and
# can be tightened later as more firmware rev responses are confirmed.
#
# Observed unit-1 response — pre-commissioning, fault 71:
# 0,4,71,2453,599,0000,000,0000,0000,00000,00000,000,211,005,000,000,000,
# 000,0008,0000,2925,0000,1,0,0,0,0,0,016
#
# Observed (2026-04-26) — post-commissioning, unit-1 (slave) querying both:
# PGS0 (master): 1,6,0,0,0000,000,0000,0000,00000,00000,000,000,536,000,
# 4,7,54,252,0,2738,0,2,0,0,1,0,0,030
# PGS1 (slave) : 1,6,0,0,0000,000,0000,0000,00000,00000,000,000,536,000,
# 2,6,54,237,0,2682,0,2,0,0,1,0,0,030
# Cross-referencing against GS at the same moment (battery_voltage=53.6 V,
# battery_capacity=54 %, master charging_current=4 A, slave=3 A):
# field_14 (idx 13) = 536 → battery voltage * 10 (V) [confirmed]
# field_18 (idx 17) = 54 → battery capacity (%) [confirmed]
# field_16 (idx 15) = 4 / 2 → likely battery_charging_current (A)
# — master 4 A matches; slave 2 vs GS 3 A
# (sample-time skew between the two reads)
# field_17 (idx 16) = 7 / 6 → small int that follows charging side;
# candidate: heat-sink temp (°C 20)?
# field_21 (idx 20) = 2738 / 2682 → 4-digit, fluctuates per-unit;
# candidate: mppt1_input_voltage*10 (was
# 270.7 / 271.8 V — close, not exact);
# might be a cumulative counter instead.
# field_1 ("parallel_unit_count") reports 6 with a 2-unit cluster — name
# is wrong; not yet identified.
#
# Next-pass plan: capture under load (AC out enabled) and during
# battery discharge to disambiguate currents/voltages from counters.
"PGS": {
"name": "PGS",
"description": "Parallel general status inquiry",
"help": " -- example: PGS0 queries parallel status for instance 0 (master)",
"command_type": CommandType.PI18_QUERY,
"result_type": ResultType.COMMA_DELIMITED,
"reading_definitions": [
# PGS field 0 has DIFFERENT semantics from GS field 27 despite sharing
# this description in upstream powermon. Live captures show this field
# always returns 1 regardless of the queried instance, so it appears to
# be a "valid response received" flag — not the instance index. Kept
# as a 2-value flag here; the GS version is the actual instance index.
{"description": "Parallel response valid", "reading_type": ReadingType.MESSAGE,
"response_type": ResponseType.LIST, "options": ["Not valid", "valid"]},
{"description": "Parallel unit count", "reading_type": ReadingType.MESSAGE},
{"description": "Fault code", "reading_type": ReadingType.MESSAGE},
{"description": "Field 4 (raw)", "reading_type": ReadingType.MESSAGE},
{"description": "Grid frequency", "reading_type": ReadingType.FREQUENCY,
"response_type": ResponseType.TEMPLATE_INT, "format_template": "r/10", "device_class": "frequency"},
{"description": "AC output voltage", "reading_type": ReadingType.VOLTS,
"response_type": ResponseType.TEMPLATE_INT, "format_template": "r/10", "device_class": "voltage"},
{"description": "AC output frequency (raw)", "reading_type": ReadingType.MESSAGE},
{"description": "AC output apparent power", "reading_type": ReadingType.APPARENT_POWER,
"response_type": ResponseType.INT, "device_class": "apparent_power"},
{"description": "AC output active power", "reading_type": ReadingType.WATTS,
"response_type": ResponseType.INT, "device_class": "power"},
{"description": "Total AC output apparent power", "reading_type": ReadingType.APPARENT_POWER, "response_type": ResponseType.INT},
{"description": "Total AC output active power", "reading_type": ReadingType.WATTS, "response_type": ResponseType.INT},
{"description": "Load percentage", "reading_type": ReadingType.PERCENTAGE, "response_type": ResponseType.INT},
{"description": "Field 13 (raw)", "reading_type": ReadingType.MESSAGE},
# Identified 2026-04-26 by cross-reference with GS battery_voltage:
{"description": "Battery voltage (parallel view)", "reading_type": ReadingType.VOLTS,
"response_type": ResponseType.TEMPLATE_INT, "format_template": "r/10", "device_class": "voltage"},
{"description": "Field 15 (raw)", "reading_type": ReadingType.MESSAGE},
{"description": "Field 16 (raw)", "reading_type": ReadingType.MESSAGE},
{"description": "Field 17 (raw)", "reading_type": ReadingType.MESSAGE},
# Identified 2026-04-26 by cross-reference with GS battery_capacity:
{"description": "Battery capacity (parallel view)", "reading_type": ReadingType.PERCENTAGE,
"response_type": ResponseType.INT},
{"description": "Field 19 (raw)", "reading_type": ReadingType.MESSAGE},
{"description": "Field 20 (raw)", "reading_type": ReadingType.MESSAGE},
{"description": "Field 21 (raw)", "reading_type": ReadingType.MESSAGE},
{"description": "Field 22 (raw)", "reading_type": ReadingType.MESSAGE},
{"description": "Flag 23 (raw)", "reading_type": ReadingType.MESSAGE},
{"description": "Flag 24 (raw)", "reading_type": ReadingType.MESSAGE},
{"description": "Flag 25 (raw)", "reading_type": ReadingType.MESSAGE},
{"description": "Flag 26 (raw)", "reading_type": ReadingType.MESSAGE},
{"description": "Flag 27 (raw)", "reading_type": ReadingType.MESSAGE},
{"description": "Flag 28 (raw)", "reading_type": ReadingType.MESSAGE},
{"description": "Field 30 (raw)", "reading_type": ReadingType.MESSAGE},
],
"test_responses": [
b"^D1130,4,71,2453,599,0000,000,0000,0000,00000,00000,000,211,005,000,000,000,000,0008,0000,2925,0000,1,0,0,0,0,0,016\x8f\xad\r",
],
"regex": "PGS(\\d+)$",
},
}
COMMANDS_TO_REMOVE = []
class PI18(AbstractProtocol):
""" pi18 protocol handler """
def __str__(self):
return "PI18 protocol handler"
def __init__(self) -> None:
super().__init__()
self.protocol_id = b"PI18"
self.add_command_definitions(QUERY_COMMANDS)
self.add_command_definitions(SETTER_COMMANDS, result_type=ResultType.PI18_ACK)
self.remove_command_definitions(COMMANDS_TO_REMOVE)
self.check_definitions_count(expected=26) # Count of all Commands
self.add_supported_ports([PortType.SERIAL, PortType.USB])
def check_crc(self, response: str, command_definition: CommandDefinition = None):
""" crc check, override for now """
log.debug("check crc for %s in pi18", response)
if response.startswith(b"^D") or response.startswith(b"^1") or response.startswith(b"^0"):
# get response CRC
data_to_check = response[:-3]
crc_high, crc_low = crc(data_to_check)
# print(crc_high, crc_low)
# print(response[-3], response[-2])
if (crc_high, crc_low) == (response[-3], response[-2]):
return True
else:
log.info("PI18 response check_crc doesnt match calc (%x, %x), got (%x, %x)", crc_high, crc_low, response[-3], response[-2])
raise InvalidCRC(f"PI18 response check_crc doesnt match calc ({crc_high:02x}, {crc_low:02x}), got ({response[-3]:02x}, {response[-2]:02x})")
else:
log.info("PI18 response doesnt start with ^D - check_crc fails")
raise InvalidResponse("PI18 response starts with invalid character - crc check fails")
log.info("PI18 response check_crc fall through")
return False
def trim_response(self, response: str, command_definition: CommandDefinition = None) -> str:
""" Remove extra characters from response """
log.debug("trim %s, definition: %s", response, command_definition)
if response.startswith(b"^D"):
# trim ^Dxxx where xxx is data length
response = response[5:]
if response.endswith(b'\r'):
# has checksum, so trim last 3 chars
response = response[:-3]
if response.startswith(b'('):
# pi30 style response
response = response[1:]
# if response.startswith(b'^1') or response.startswith(b'^0'):
# # ACK / NACK response
# response = response[1:]
return response
def get_full_command(self, command: str) -> bytes:
""" generate the full command including prefix, crc and \n as needed """
log.info("Using protocol: %s with %i commands", self.protocol_id, len(self.command_definitions))
command_defn = self.get_command_definition(command)
# raise exception if no command definition is found
if command_defn is None:
raise CommandDefinitionMissing(f"No definition found in PI18 for {command}")
# full command is ^PlllCCCcrc\n or ^SlllCCCcrc\n
# lll = length of all except ^Dlll
# CCC = command
# crc = 2 bytes
length = len(command) + 3
# Determine prefix
match command_defn.command_type:
case CommandType.PI18_QUERY:
prefix = "^P"
case CommandType.PI18_SETTER:
prefix = "^S"
case _:
# edge case / default PI30 command / maybe this should raise an error
prefix = "("
full_command = bytes(f"{prefix}{length:#03d}{command}", "utf-8")
crc_high, crc_low = crc(full_command)
full_command += bytes([crc_high, crc_low, 13])
log.debug("full command: %s", full_command)
return full_command