178 lines
11 KiB
Markdown
178 lines
11 KiB
Markdown
|
|
# 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`](./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
|
||
|
|
|
||
|
|
```bash
|
||
|
|
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.
|
||
|
|
|
||
|
|
## 2. udev rule + serial-keyed symlinks
|
||
|
|
|
||
|
|
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`](./etc/udev/rules.d/99-lvx6048.rules). Deploy:
|
||
|
|
|
||
|
|
```bash
|
||
|
|
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
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2b. `lvx-resolve-links` — serial → symlink resolver
|
||
|
|
|
||
|
|
[`bin/lvx-resolve-links`](./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:
|
||
|
|
|
||
|
|
```bash
|
||
|
|
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`](./lvx-flash/flash.py):
|
||
|
|
|
||
|
|
```bash
|
||
|
|
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
|
||
|
|
|
||
|
|
```bash
|
||
|
|
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/powermon.yaml), [`config/powermon/powermon2.yaml`](./config/powermon/powermon2.yaml). The snapshots contain `<MQTT_BROKER_IP>` / `<MQTT_USER>` / `<MQTT_PASSWORD>` placeholders — fill them in after copying into place. Deploy:
|
||
|
|
|
||
|
|
```bash
|
||
|
|
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/`](./powermon-patches/) (see [its README](./powermon-patches/README.md) for per-file detail). To apply manually:
|
||
|
|
|
||
|
|
```bash
|
||
|
|
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_COMMANDS` — `FWS` (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`](./etc/systemd/system/powermon.service),
|
||
|
|
[`powermon2.service`](./etc/systemd/system/powermon2.service),
|
||
|
|
[`lvx-resolve-links.service`](./etc/systemd/system/lvx-resolve-links.service),
|
||
|
|
[`powermon.service.d/10-resolver.conf`](./etc/systemd/system/powermon.service.d/10-resolver.conf) (and its sibling under `powermon2.service.d/`).
|
||
|
|
|
||
|
|
Deploy:
|
||
|
|
|
||
|
|
```bash
|
||
|
|
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
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# 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:
|
||
|
|
|
||
|
|
```bash
|
||
|
|
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.
|