708 lines
40 KiB
Python
708 lines
40 KiB
Python
""" 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
|