Files
shaggy-solar/LVX6048/Install.md
2026-04-24 16:34:10 -04:00

11 KiB

LVX6048 → HA Monitoring Install

Target: Debian-family Linux (developed on Raspberry Pi CM5), two LVX6048s over USB, HA MQTT broker on the LAN.

Shortcut: the top-level install.sh automates every section below. This document explains what it does and why, and how to do each step by hand if you need to diverge.

Path conventions in the snippets below use $BASE to mean the root of this package (e.g. ~/solar/LVX6048). Every canonical file lives under $BASE/ mirroring its system destination path — e.g. $BASE/etc/udev/rules.d/99-lvx6048.rules deploys to /etc/udev/rules.d/99-lvx6048.rules.

1. Install powermon

uv tool install --with bleak powermon
powermon --listProtocols   # confirm PI18 present

mpp-solar is yanked from PyPI; powermon replaces it for both adhoc and daemon use.

The LVX6048 reports no USB serial string in the USB descriptor, so vid:pid matching applies to both units equally. Rather than pin devices by USB hub port (which breaks the moment you move a cable), we use the PI18 ID query — which does return each inverter's real serial number — and build stable /dev/lvx6048-{1,2} symlinks from that.

2a. udev rule — dialout access

The rule grants dialout access to any LVX6048 hidraw; the logical "which unit is this?" decision happens in lvx-resolve-links (next step).

Canonical: etc/udev/rules.d/99-lvx6048.rules. Deploy:

sudo install -m 644 "$BASE/etc/udev/rules.d/99-lvx6048.rules" /etc/udev/rules.d/99-lvx6048.rules
sudo udevadm control --reload-rules && sudo udevadm trigger --subsystem-match=hidraw
ls -l /dev/hidraw*       # both should be group=dialout, mode 0660

bin/lvx-resolve-links runs once at boot, globs /dev/hidraw*, sends PI18 ID to each, and creates:

/dev/lvx6048-1 → /dev/hidrawX   (where X matches SERIAL_UNIT_1)
/dev/lvx6048-2 → /dev/hidrawX   (where X matches SERIAL_UNIT_2)

A single resolver pass, exclusive, before any powermon service starts — sidesteps the collision you get if each service probes independently (each probe open-read-close would race the sibling service already holding the hidraw fd).

Deploy the script + systemd oneshot:

sudo install -m 755 "$BASE/bin/lvx-resolve-links" /usr/local/sbin/lvx-resolve-links
# rewrite the shebang in the installed copy to point at powermon's venv python
sudo sed -i "1c#!$HOME/.local/share/uv/tools/powermon/bin/python" /usr/local/sbin/lvx-resolve-links
sudo install -m 644 "$BASE/etc/systemd/system/lvx-resolve-links.service" /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now lvx-resolve-links.service
ls -l /dev/lvx6048-*     # both symlinks should now exist

2c. Capturing a unit's serial

If you ever swap an inverter, capture the new serial and update SERIAL_UNIT_* in both /usr/local/sbin/lvx-resolve-links and lvx-flash/flash.py:

sudo systemctl stop powermon.service powermon2.service lvx-resolve-links.service
TMP=$(mktemp --suffix=.yaml)
printf 'loop: once\ndevice:\n  name: probe\n  port: {type: usb, path: /dev/hidraw0, protocol: PI18}\ncommands:\n  - {command: ID, trigger: {loops: 1}}\n' > "$TMP"
~/.local/bin/powermon -C "$TMP" 2>&1 | grep serial_number
rm -f "$TMP"
sudo systemctl start lvx-resolve-links.service powermon.service powermon2.service

The two current serials are hard-coded as SERIAL_UNIT_1 / SERIAL_UNIT_2 in both files.

3. Smoke test

powermon -C "$BASE/smoketest/console.yaml"      # continuous console output (unit #1)
# or one-shot against either unit:
powermon --adhoc GS -C ~/.config/powermon/powermon.yaml
powermon --adhoc GS -C ~/.config/powermon/powermon2.yaml

4. powermon configs

Powermon only supports one device: per config file, so each inverter needs its own config + its own systemd unit.

Canonical (mode 600): config/powermon/powermon.yaml, config/powermon/powermon2.yaml. The snapshots contain <MQTT_BROKER_IP> / <MQTT_USER> / <MQTT_PASSWORD> placeholders — fill them in after copying into place. Deploy:

mkdir -p ~/.config/powermon
install -m 600 "$BASE/config/powermon/powermon.yaml"  ~/.config/powermon/powermon.yaml
install -m 600 "$BASE/config/powermon/powermon2.yaml" ~/.config/powermon/powermon2.yaml
# edit the two files: replace <MQTT_BROKER_IP>, <MQTT_USER>, <MQTT_PASSWORD> placeholders

Key points (same for both, differ only in device name / path / topic prefix):

  • loop: 1 at top level — without it, powermon defaults to "once" and exits after one pass
  • device.port.path: /dev/lvx6048-1 (or -2) — resolved via the resolver symlinks from §2b, not via powermon's own wildcard-+-serial probing (which collides when two services run simultaneously)
  • device.port.protocol: PI18 — PI30 returns CRC errors on this unit
  • MQTT username/password are HA Mosquitto add-on creds (broker rejects anonymous by default)
  • One hass-formatted MQTT output per command; ~28 entities auto-discovered per inverter, published at homeassistant/sensor/lvx6048_{1,2}_<field>/state
  • entity_id_prefix / topic = lvx6048_1 for unit #1, lvx6048_2 for unit #2

Poll cadence: GS/5s, MOD/10s, ET/60s, PIRI/300s.

5. Local patches (pinned to powermon 1.0.18, survive until uv tool upgrade)

The six patches are shipped as full file drop-ins under powermon-patches/ (see its README for per-file detail). To apply manually:

POWERMON_SITE="$(ls -d ~/.local/share/uv/tools/powermon/lib/python*/site-packages/powermon)"
install -m 644 "$BASE/powermon-patches/pi18.py"              "$POWERMON_SITE/protocols/pi18.py"
install -m 644 "$BASE/powermon-patches/usbport.py"           "$POWERMON_SITE/ports/usbport.py"
install -m 644 "$BASE/powermon-patches/mqttbroker.py"        "$POWERMON_SITE/libs/mqttbroker.py"
install -m 644 "$BASE/powermon-patches/port_config_model.py" "$POWERMON_SITE/configmodel/port_config_model.py"
install -m 644 "$BASE/powermon-patches/ports_init.py"        "$POWERMON_SITE/ports/__init__.py"

What each patch fixes:

a. protocols/pi18.py ~line 323: "DC/AC power direction""DC-AC power direction". The slash creates a bogus extra MQTT topic level.

b. ports/usbport.py send_and_receive(): (1) drain any leftover bytes from the hidraw fd before sending (non-blocking read loop swallowing BlockingIOError); (2) wrap the main os.read in the retry loop with try: … except BlockingIOError: continue. Without (1), a prior command's late HID bytes get parsed as the next command's reply → KeyError in response decoding. Without (2), an empty buffer on first read aborts the command.

c. libs/mqttbroker.py: broaden connect()'s except ConnectionRefusedError to except (ConnectionRefusedError, OSError), and narrow publish()'s bare except Exception to except (OSError, RuntimeError, ValueError) with a clearer log message. Without this, any network blip to the broker (e.g. HA restart, Errno 113 No route to host) crashes the daemon.

d. protocols/pi18.py: append two new query commands to QUERY_COMMANDSFWS (Fault and Warning Status, fault code + ~32 warning bits, cross-referenced with PI30 QPGS fault codes including 71 "Parallel version different" and the rest of the 8x parallel-comm family) and PGS (Parallel General Status, regex PGS(\d+)$, LVX6048-specific 30-field layout with only the high-confidence fields named and the rest exposed as raw strings). Bump check_definitions_count(expected=24) to 26. These are used by lvx-flash sync-check.

e. configmodel/port_config_model.py: add serial_number: None | str | int = Field(default=None) to UsbPortConfig. The model is NoExtraBaseModel (extra-forbidden), so powermon rejects the serial_number: config key without this even though USBPort.resolve_path already consumes it.

f. ports/__init__.py from_config(): change port_config['serial_number'] = serial_number to be a fallback (if port_config.get('serial_number') is None:). The device-level serial_number is the logical HA identifier (lvx6048_1), the port-level one is the hardware PI18 serial (22-digit) — different fields, different purposes; they must not be conflated.

Patches (e) and (f) enable powermon's native wildcard-path + serial-matching flow, which we don't currently use — two services probing independently at startup race each other over the same hidraw. Our current setup uses the external lvx-resolve-links resolver (§2b) instead, so (e) and (f) are dormant but kept applied in case we ever want to use powermon's native resolver for a single-service deployment.

6. systemd services

One powermon unit per inverter, both gated on lvx-resolve-links.service. Canonical: etc/systemd/system/powermon.service, powermon2.service, lvx-resolve-links.service, powermon.service.d/10-resolver.conf (and its sibling under powermon2.service.d/).

Deploy:

sudo install -m 644 "$BASE/etc/systemd/system/powermon.service"  /etc/systemd/system/
sudo install -m 644 "$BASE/etc/systemd/system/powermon2.service" /etc/systemd/system/
sudo mkdir -p /etc/systemd/system/powermon.service.d /etc/systemd/system/powermon2.service.d
sudo install -m 644 "$BASE/etc/systemd/system/powermon.service.d/10-resolver.conf" \
                    /etc/systemd/system/powermon.service.d/10-resolver.conf
sudo install -m 644 "$BASE/etc/systemd/system/powermon2.service.d/10-resolver.conf" \
                    /etc/systemd/system/powermon2.service.d/10-resolver.conf
sudo systemctl daemon-reload
sudo systemctl enable --now powermon.service powermon2.service
journalctl -u powermon.service -u powermon2.service -f

The drop-in adds After=lvx-resolve-links.service + Requires=lvx-resolve-links.service so both powermon services wait for the symlinks before starting.

7. Verify

# live sample of what both units are publishing (12 s = ~2 full GS cycles per unit)
timeout 15 mosquitto_sub -h 10.0.0.41 -u mqtt -P <pass> \
    -t 'homeassistant/sensor/+/state' -W 12 -v | grep lvx6048_

Should show ~58 messages (~29 per unit per GS cycle) over that window, with both lvx6048_1_* and lvx6048_2_* entities. In HA: Settings → Devices & Services → MQTT → two devices LVX6048 (and LVX6048 #2), each with ~29 auto-discovered sensors.

8. Moving cables

Because identification is PI18-serial-based, cable / USB hub moves don't require config changes. After any cable shuffle:

sudo systemctl restart lvx-resolve-links.service powermon.service powermon2.service

The resolver re-probes, symlinks update, powermon services latch onto the correct device by symlink. No hardcoded hub-port references anywhere in the stack.