|
|
@@ -694,6 +694,14 @@ INVERTER_WORKSTATE = {
|
|
|
6: "GridCharging",
|
|
|
}
|
|
|
|
|
|
+
|
|
|
+def RELAY_STATE(v: int) -> str:
|
|
|
+ """Decode relay-state registers (25237..25242, 15211, 15212).
|
|
|
+
|
|
|
+ Per the protocol: 0=Disconnect, 1=Connect.
|
|
|
+ """
|
|
|
+ return "Closed" if v == 1 else "Open"
|
|
|
+
|
|
|
BATTERY_TYPE = {
|
|
|
0: "AGM",
|
|
|
1: "Flooded",
|
|
|
@@ -730,33 +738,103 @@ class Register:
|
|
|
unit: str = ""
|
|
|
|
|
|
|
|
|
-# CHARGER block
|
|
|
+# CHARGER block (registers 15201..15221; gaps at 15204, 15213, 15214
|
|
|
+# are reserved / unused by the firmware)
|
|
|
CHARGER_REGS: List[Register] = [
|
|
|
+ # ----- Operating state -----
|
|
|
Register(15201, "Charger workstate", decode=lambda v: CHARGER_WORKSTATE.get(v, f"unknown({v})")),
|
|
|
Register(15202, "MPPT state", decode=lambda v: MPPT_STATE.get(v, f"unknown({v})")),
|
|
|
Register(15203, "Charging state", decode=lambda v: CHARGING_STATE.get(v, f"unknown({v})")),
|
|
|
+ # ----- Live measurements -----
|
|
|
Register(15205, "PV voltage", scale=0.1, unit="V"),
|
|
|
- Register(15206, "Battery voltage", scale=0.1, unit="V"),
|
|
|
+ Register(15206, "Battery voltage (charger)", scale=0.1, unit="V"),
|
|
|
Register(15207, "Charger current", scale=0.1, unit="A"),
|
|
|
Register(15208, "Charger power", unit="W"),
|
|
|
- # 15211 Battery Relay, 15212 PV Relay -- not exposed by reference config
|
|
|
+ # ----- Sensors -----
|
|
|
+ Register(15209, "Charger radiator temp", unit="°C"),
|
|
|
+ Register(15210, "External temp", unit="°C"),
|
|
|
+ # ----- Relays -----
|
|
|
+ Register(15211, "Battery Relay (charger)", decode=RELAY_STATE),
|
|
|
+ Register(15212, "PV Relay (charger)", decode=RELAY_STATE),
|
|
|
+ # ----- Configuration / identity -----
|
|
|
+ Register(15215, "Battery voltage grade", decode=lambda v: f"{v}V system"),
|
|
|
+ Register(15216, "Charger rated current", scale=0.1, unit="A"),
|
|
|
+ # ----- Energy accumulator (high * 1000 kWh + low * 0.1 kWh) -----
|
|
|
+ Register(15217, "Acc PV energy MWh part", unit="MWh part"),
|
|
|
+ Register(15218, "Acc PV energy kWh part", scale=0.1, unit="kWh part"),
|
|
|
+ # ----- Runtime since last reset -----
|
|
|
+ Register(15219, "Acc runtime days", unit="d"),
|
|
|
+ Register(15220, "Acc runtime hours", unit="h"),
|
|
|
+ Register(15221, "Acc runtime minutes", unit="m"),
|
|
|
]
|
|
|
|
|
|
-# INVERTER block (read-only telemetry)
|
|
|
+
|
|
|
+# INVERTER block (registers 25201..25274 with gaps at 25204, 25220,
|
|
|
+# 25224, 25227, 25228, 25232, 25236, 25243, 25244, 25262..25268, 25270..25272,
|
|
|
+# 25275, 25276).
|
|
|
INVERTER_REGS: List[Register] = [
|
|
|
+ # ----- Operating state -----
|
|
|
Register(25201, "Inverter Work state", decode=lambda v: INVERTER_WORKSTATE.get(v, f"unknown({v})")),
|
|
|
- Register(25205, "Battery voltage", scale=0.1, unit="V"),
|
|
|
+ Register(25202, "AC voltage grade", decode=lambda v: f"{v}V variant"),
|
|
|
+ Register(25203, "Rated power", unit="VA"),
|
|
|
+ # ----- Voltages -----
|
|
|
+ Register(25205, "Battery voltage (inverter)", scale=0.1, unit="V"),
|
|
|
Register(25206, "Inverter voltage", scale=0.1, unit="V"),
|
|
|
Register(25207, "Grid voltage", scale=0.1, unit="V"),
|
|
|
- Register(25213, "Inverter power", signed=True, unit="W"),
|
|
|
- Register(25214, "Grid power", signed=True, unit="W"),
|
|
|
- Register(25215, "Load power", signed=True, unit="W"),
|
|
|
- Register(25216, "System load", unit="%"),
|
|
|
- Register(25233, "AC radiator temp", unit="°C"),
|
|
|
+ Register(25208, "BUS voltage", scale=0.1, unit="V"),
|
|
|
+ # ----- Currents -----
|
|
|
+ Register(25209, "Control current", scale=0.1, unit="A"),
|
|
|
+ Register(25210, "Inverter current", scale=0.1, unit="A"),
|
|
|
+ Register(25211, "Grid current", scale=0.1, unit="A"),
|
|
|
+ Register(25212, "Load current", scale=0.1, unit="A"),
|
|
|
+ # ----- Real power (P) -----
|
|
|
+ Register(25213, "PInverter", signed=True, unit="W"),
|
|
|
+ Register(25214, "PGrid", signed=True, unit="W"),
|
|
|
+ Register(25215, "PLoad", signed=True, unit="W"),
|
|
|
+ Register(25216, "Load percent", unit="%"),
|
|
|
+ # ----- Apparent power (S) -----
|
|
|
+ Register(25217, "SInverter", signed=True, unit="VA"),
|
|
|
+ Register(25218, "SGrid", signed=True, unit="VA"),
|
|
|
+ Register(25219, "SLoad", signed=True, unit="VA"),
|
|
|
+ # ----- Reactive power (Q) -----
|
|
|
+ Register(25221, "QInverter", signed=True, unit="var"),
|
|
|
+ Register(25222, "QGrid", signed=True, unit="var"),
|
|
|
+ Register(25223, "QLoad", signed=True, unit="var"),
|
|
|
+ # ----- Frequencies -----
|
|
|
+ Register(25225, "Inverter frequency", scale=0.01, unit="Hz"),
|
|
|
+ Register(25226, "Grid frequency", scale=0.01, unit="Hz"),
|
|
|
+ # ----- Temperatures -----
|
|
|
+ Register(25233, "AC radiator temp", unit="°C"),
|
|
|
Register(25234, "Transformer temp", unit="°C"),
|
|
|
Register(25235, "DC radiator temp", unit="°C"),
|
|
|
+ # ----- Relays -----
|
|
|
+ Register(25237, "Inverter relay", decode=RELAY_STATE),
|
|
|
+ Register(25238, "Grid relay", decode=RELAY_STATE),
|
|
|
+ Register(25239, "Load relay", decode=RELAY_STATE),
|
|
|
+ Register(25240, "N_Line relay", decode=RELAY_STATE),
|
|
|
+ Register(25241, "DC relay", decode=RELAY_STATE),
|
|
|
+ Register(25242, "Earth relay", decode=RELAY_STATE),
|
|
|
+ # ----- Energy accumulators (each = high*1000 kWh + low*0.1 kWh) -----
|
|
|
+ Register(25245, "Acc charger MWh part", unit="MWh part"),
|
|
|
+ Register(25246, "Acc charger kWh part", scale=0.1, unit="kWh part"),
|
|
|
+ Register(25247, "Acc discharger MWh part", unit="MWh part"),
|
|
|
+ Register(25248, "Acc discharger kWh part", scale=0.1, unit="kWh part"),
|
|
|
+ Register(25249, "Acc grid-buy MWh part", unit="MWh part"),
|
|
|
+ Register(25250, "Acc grid-buy kWh part", scale=0.1, unit="kWh part"),
|
|
|
+ Register(25251, "Acc grid-sell MWh part", unit="MWh part"),
|
|
|
+ Register(25252, "Acc grid-sell kWh part", scale=0.1, unit="kWh part"),
|
|
|
+ Register(25253, "Acc load MWh part", unit="MWh part"),
|
|
|
+ Register(25254, "Acc load kWh part", scale=0.1, unit="kWh part"),
|
|
|
+ Register(25255, "Acc self-use MWh part", unit="MWh part"),
|
|
|
+ Register(25256, "Acc self-use kWh part", scale=0.1, unit="kWh part"),
|
|
|
+ Register(25257, "Acc PV-sell MWh part", unit="MWh part"),
|
|
|
+ Register(25258, "Acc PV-sell kWh part", scale=0.1, unit="kWh part"),
|
|
|
+ Register(25259, "Acc grid-charge MWh part", unit="MWh part"),
|
|
|
+ Register(25260, "Acc grid-charge kWh part", scale=0.1, unit="kWh part"),
|
|
|
+ # ----- Battery telemetry (live) -----
|
|
|
Register(25273, "Battery power", signed=True, unit="W"),
|
|
|
- Register(25274, "Battery current", signed=True, scale=0.1, unit="A"),
|
|
|
+ Register(25274, "Battery current", signed=True, scale=0.1, unit="A"),
|
|
|
+ Register(25275, "Battery voltage grade", decode=lambda v: f"{v}V system"),
|
|
|
]
|
|
|
|
|
|
# SETTINGS block (read-only mirror of writable config)
|
|
|
@@ -830,19 +908,86 @@ def poll_individual(t: Transport, slave: int,
|
|
|
return results
|
|
|
|
|
|
|
|
|
-def print_snapshot(snapshot: List[Tuple[str, Any, str]], title: str) -> None:
|
|
|
+def _split_groups(snapshot: List[Tuple[str, Any, str]],
|
|
|
+ headings: List[Tuple[str, Any]]) -> List[Tuple[str, List[Tuple[str, Any, str]]]]:
|
|
|
+ """Split a flat snapshot into labelled groups.
|
|
|
+
|
|
|
+ `headings` is a list of `(heading_text, name_prefix_or_prefixes)`
|
|
|
+ pairs. Each register whose name starts with one of the listed
|
|
|
+ prefixes goes into that group. A prefix can also be a `str` for
|
|
|
+ the common single-prefix case. Registers that don't match any
|
|
|
+ prefix are silently dropped.
|
|
|
+ """
|
|
|
+ out: List[Tuple[str, List[Tuple[str, Any, str]]]] = []
|
|
|
+ for header, prefixes in headings:
|
|
|
+ if isinstance(prefixes, str):
|
|
|
+ prefixes = [prefixes]
|
|
|
+ items = [(n, v, u) for n, v, u in snapshot
|
|
|
+ if any(n.startswith(p) for p in prefixes)]
|
|
|
+ if items:
|
|
|
+ out.append((header, items))
|
|
|
+ return out
|
|
|
+
|
|
|
+
|
|
|
+def _combine_accumulators(snapshot: List[Tuple[str, Any, str]]) -> List[Tuple[str, Any, str]]:
|
|
|
+ """Merge every 'Acc <name> MWh part' / 'kWh part' pair into a single
|
|
|
+ 'Acc <name> (total) = X.X kWh' line. Returns the merged list (in
|
|
|
+ the original order, with the combined line replacing the two
|
|
|
+ originals)."""
|
|
|
+ by_name = {n: (v, u) for n, v, u in snapshot}
|
|
|
+ out: List[Tuple[str, Any, str]] = []
|
|
|
+ skip = set()
|
|
|
+ for name, value, unit in snapshot:
|
|
|
+ if name in skip:
|
|
|
+ continue
|
|
|
+ if name.endswith(" MWh part"):
|
|
|
+ base = name[: -len(" MWh part")]
|
|
|
+ kwh_name = base + " kWh part"
|
|
|
+ if kwh_name in by_name:
|
|
|
+ kwh_val = by_name[kwh_name][0]
|
|
|
+ total_kwh = value * 1000.0 + kwh_val * 0.1
|
|
|
+ # `base` looks like "Acc <name>"; we want "<name>".
|
|
|
+ if base.startswith("Acc "):
|
|
|
+ label = base[len("Acc "):]
|
|
|
+ else:
|
|
|
+ label = base
|
|
|
+ out.append((f"Acc {label.strip()} (total)",
|
|
|
+ round(total_kwh, 2), "kWh"))
|
|
|
+ skip.add(name)
|
|
|
+ skip.add(kwh_name)
|
|
|
+ continue
|
|
|
+ out.append((name, value, unit))
|
|
|
+ return out
|
|
|
+
|
|
|
+
|
|
|
+def print_snapshot(snapshot: List[Tuple[str, Any, str]], title: str,
|
|
|
+ groups: Optional[List[Tuple[str, List[Tuple[str, Any, str]]]]] = None) -> None:
|
|
|
+ """Print a snapshot. If `groups` is given, print section headers between
|
|
|
+ groups; otherwise dump the whole list flat."""
|
|
|
bar = "-" * max(15, len(title) + 4)
|
|
|
print(bar)
|
|
|
print(f" {title}")
|
|
|
print(bar)
|
|
|
name_w = max(len(n) for n, _, _ in snapshot)
|
|
|
- for name, value, unit in snapshot:
|
|
|
- if isinstance(value, float):
|
|
|
- vstr = f"{value:.1f}"
|
|
|
- else:
|
|
|
- vstr = str(value)
|
|
|
- suffix = f"{unit}" if unit else ""
|
|
|
- print(f" {name:<{name_w}} = {vstr}{suffix}")
|
|
|
+ if groups:
|
|
|
+ for header, items in groups:
|
|
|
+ print(f" --- {header} ---")
|
|
|
+ for name, value, unit in items:
|
|
|
+ if isinstance(value, float):
|
|
|
+ vstr = f"{value:.1f}"
|
|
|
+ else:
|
|
|
+ vstr = str(value)
|
|
|
+ suffix = f"{unit}" if unit else ""
|
|
|
+ print(f" {name:<{name_w}} = {vstr}{suffix}")
|
|
|
+ print()
|
|
|
+ else:
|
|
|
+ for name, value, unit in snapshot:
|
|
|
+ if isinstance(value, float):
|
|
|
+ vstr = f"{value:.1f}"
|
|
|
+ else:
|
|
|
+ vstr = str(value)
|
|
|
+ suffix = f"{unit}" if unit else ""
|
|
|
+ print(f" {name:<{name_w}} = {vstr}{suffix}")
|
|
|
print()
|
|
|
|
|
|
|
|
|
@@ -998,8 +1143,37 @@ def main() -> int:
|
|
|
print(json.dumps(payload, ensure_ascii=False))
|
|
|
else:
|
|
|
print(f"=== {now} ===")
|
|
|
- print_snapshot(charger, "CHARGER")
|
|
|
- print_snapshot(inverter, "INVERTER")
|
|
|
+ # Combine accumulator pairs (MWh part + kWh part -> total kWh)
|
|
|
+ # and split CHARGER/INVERTER into readable sub-sections.
|
|
|
+ charger = _combine_accumulators(charger)
|
|
|
+ inverter = _combine_accumulators(inverter)
|
|
|
+
|
|
|
+ charger_groups = _split_groups(charger, [
|
|
|
+ ("State", ["Charger workstate", "MPPT state", "Charging state"]),
|
|
|
+ ("Live readings", ["PV voltage", "Battery voltage (charger)", "Charger current", "Charger power"]),
|
|
|
+ ("Sensors", ["Charger radiator temp", "External temp"]),
|
|
|
+ ("Relays", ["Battery Relay (charger)", "PV Relay (charger)"]),
|
|
|
+ ("Configuration", ["Battery voltage grade", "Charger rated current"]),
|
|
|
+ ("Energy totals", ["Acc PV energy"]),
|
|
|
+ ("Runtime", ["Acc runtime "]),
|
|
|
+ ])
|
|
|
+ inverter_groups = _split_groups(inverter, [
|
|
|
+ ("State", ["Inverter Work state"]),
|
|
|
+ ("Configuration", ["AC voltage grade", "Rated power", "Battery voltage grade"]),
|
|
|
+ ("Voltages", ["Battery voltage (inverter)", "Inverter voltage", "Grid voltage", "BUS voltage"]),
|
|
|
+ ("Currents", ["Control current", "Inverter current", "Grid current", "Load current"]),
|
|
|
+ ("Real power (P)", ["PInverter", "PGrid", "PLoad", "Load percent"]),
|
|
|
+ ("Apparent power (S)", ["SInverter", "SGrid", "SLoad"]),
|
|
|
+ ("Reactive power (Q)", ["QInverter", "QGrid", "QLoad"]),
|
|
|
+ ("Frequencies", ["Inverter frequency", "Grid frequency"]),
|
|
|
+ ("Temperatures", ["AC radiator temp", "Transformer temp", "DC radiator temp"]),
|
|
|
+ ("Relays", ["Inverter relay", "Grid relay", "Load relay", "N_Line relay", "DC relay", "Earth relay"]),
|
|
|
+ ("Energy totals", ["Acc "]),
|
|
|
+ ("Battery telemetry", ["Battery power", "Battery current"]),
|
|
|
+ ])
|
|
|
+
|
|
|
+ print_snapshot(charger, "CHARGER", groups=charger_groups)
|
|
|
+ print_snapshot(inverter, "INVERTER", groups=inverter_groups)
|
|
|
print_snapshot(settings, "SETTINGS")
|
|
|
sys.stdout.flush()
|
|
|
|