|
| 1 | +# Solar Assistant Field Mapping (reference for lux-mon parity) |
| 2 | + |
| 3 | +Captured 2026-08-21 from a live Solar Assistant install (Raspberry Pi, |
| 4 | +`192.168.12.12`, SA v2.0.0) and cross-referenced against lux-mon's register |
| 5 | +map and the EG4/LuxPower Modbus protocol doc. |
| 6 | + |
| 7 | +## Solar Assistant architecture |
| 8 | + |
| 9 | +Solar Assistant runs the **same stack as lux-mon**: |
| 10 | + |
| 11 | +| Port | Service | Notes | |
| 12 | +|------|---------|-------| |
| 13 | +| 80 | `beam.smp` (Elixir/Phoenix) | Main SA web app + API | |
| 14 | +| 8899 | `beam.smp` | Second SA listener (raw TCP, not HTTP) | |
| 15 | +| 1883 | `mosquitto` | MQTT broker (anonymous access allowed) | |
| 16 | +| 3000 | `grafana-server` | Grafana (localhost only) | |
| 17 | +| 8086 | `influxd` | InfluxDB v1.8 (localhost only) | |
| 18 | +| 8088 | `influxd` | InfluxDB RPC (localhost only) | |
| 19 | +| 5353 | `avahi-daemon` | mDNS, publishes `_solar-assistant._tcp` | |
| 20 | + |
| 21 | +The Elixir app self-extracts to `/dev/shm/grafana-sync/<hash>/` and its |
| 22 | +persistent Grafana dashboards live in `/etc/grafana/solar-assistant/dashboards/`. |
| 23 | + |
| 24 | +## InfluxDB schema (SA measurement → field) |
| 25 | + |
| 26 | +SA uses **one measurement per metric**, with a single field: |
| 27 | +- `inverter_0` for single-inverter values |
| 28 | +- `combined` for site-wide/aggregate values |
| 29 | + |
| 30 | +Full measurement list (35 total): |
| 31 | + |
| 32 | +``` |
| 33 | +AC output voltage Battery current Battery power |
| 34 | +Battery power hourly Battery power in hourly Battery power out hourly |
| 35 | +Battery state of charge Battery temperature Battery voltage |
| 36 | +CPU temperature Cloud cover Free storage |
| 37 | +Grid frequency Grid power Grid power hourly |
| 38 | +Grid power in hourly Grid power out hourly Grid voltage |
| 39 | +Inverter temperature Load power Load power essential |
| 40 | +Load power hourly Load power non-essential Outside temperature |
| 41 | +PV current 1 PV current 2 PV power |
| 42 | +PV power 1 PV power 2 PV power hourly |
| 43 | +PV power predicted PV power predicted hourly PV voltage 1 |
| 44 | +PV voltage 2 |
| 45 | +``` |
| 46 | + |
| 47 | +## Field → register mapping (SA vs lux-mon) |
| 48 | + |
| 49 | +| SA measurement | SA field | lux-mon register | lux-mon name | Match | |
| 50 | +|---|---|---|---|---| |
| 51 | +| Battery voltage | inverter_0 | 4 | battery_voltage | ✅ | |
| 52 | +| Battery state of charge | combined | 5 | soc | ✅ | |
| 53 | +| Battery power | combined | 10/11 | charge/discharge_power | ✅ (sign) | |
| 54 | +| Battery current | inverter_0 | 98 | battery_current | ✅ (signed) | |
| 55 | +| Battery temperature | combined | 67 | temp_battery | ✅ | |
| 56 | +| PV power 1 | inverter_0 | 7 | pv1_power | ✅ | |
| 57 | +| PV power 2 | inverter_0 | 8 | pv2_power | ✅ | |
| 58 | +| PV voltage 1 | inverter_0 | 1 | pv1_voltage | ✅ | |
| 59 | +| PV voltage 2 | inverter_0 | 2 | pv2_voltage | ✅ | |
| 60 | +| PV current 1 | inverter_0 | — | (derived) | ⚠️ see below | |
| 61 | +| PV current 2 | inverter_0 | — | (derived) | ⚠️ see below | |
| 62 | +| Grid voltage | inverter_0 | 12 | grid_voltage_r | ✅ | |
| 63 | +| Grid frequency | inverter_0 | 15 | grid_frequency | ✅ | |
| 64 | +| Grid power | combined | 26/27 | grid_export/import_power | ✅ | |
| 65 | +| Load power | combined | 24 | eps_power | ✅ | |
| 66 | +| AC output voltage | inverter_0 | 20 | eps_voltage_r | ✅ | |
| 67 | +| Inverter temperature | inverter_0 | **66** | temp_radiator_2 | ⚠️ see below | |
| 68 | +| Outside temperature | combined | — | (not from inverter) | — | |
| 69 | +| Cloud cover | combined | — | (not from inverter) | — | |
| 70 | +| PV power predicted | combined | — | (not from inverter) | — | |
| 71 | +| CPU temperature | combined | — | (Pi CPU, not inverter) | — | |
| 72 | + |
| 73 | +## Key findings |
| 74 | + |
| 75 | +### 1. "Inverter temperature" is a labeling difference, NOT a bug |
| 76 | + |
| 77 | +SA's "Inverter temperature" reads **register 66 (Radiator 2 / heat-sink 2)**, |
| 78 | +not register 64 (internal temperature). |
| 79 | + |
| 80 | +- SA "Inverter temperature" = 102.2°F = 39°C = lux-mon `temp_radiator_2` (reg 66) |
| 81 | +- lux-mon `temp_inverter` = 91.4°F = 33°C = reg 64 (internal temp) |
| 82 | + |
| 83 | +Per the EG4 protocol doc, reg 64 = "Internal Temp", reg 65/66 = "Radiator 1/2 |
| 84 | +Temp" (heat sinks). **lux-mon is correct**; SA is mislabeling the heat-sink |
| 85 | +temperature as "Inverter temperature". lux-mon exposes both `temp_inverter` |
| 86 | +(reg 64) and `temp_radiator_2` (reg 66) separately, which is strictly more |
| 87 | +correct. |
| 88 | + |
| 89 | +### 2. Battery current is signed (negative = discharge) |
| 90 | + |
| 91 | +Register 98 (`battery_current`) is signed. When discharging, the raw value is |
| 92 | +two's-complement negative (e.g. raw 65432 → -10.4 A). |
| 93 | + |
| 94 | +**Bug fixed 2026-08-21:** `PassiveCollector._clamp_values` was clamping *any* |
| 95 | +negative value to 0.0, which zeroed out `battery_current` during discharge. |
| 96 | +Added `_SIGNED_FIELDS = {"battery_current"}` so signed fields are not |
| 97 | +zero-clamped. |
| 98 | + |
| 99 | +### 3. Fields SA derives that lux-mon does not (yet) have |
| 100 | + |
| 101 | +These are not raw inverter registers — SA computes or sources them elsewhere: |
| 102 | + |
| 103 | +- **PV current 1/2** — SA reports these; lux-mon has `pv1_voltage`/`pv2_voltage` |
| 104 | + but no `pv1_current`/`pv2_current` register. Current can be derived as |
| 105 | + `power / voltage` per MPPT string. |
| 106 | +- **Outside temperature** — sourced from a weather API, not the inverter. |
| 107 | +- **Cloud cover** — sourced from a weather API. |
| 108 | +- **PV power predicted** — sourced from a weather/solar forecast API. |
| 109 | +- **CPU temperature** — the Pi's own CPU temp, not the inverter. |
| 110 | +- **Free storage** — the Pi's disk free space. |
| 111 | +- **Hourly rollups** (`* hourly`, `* in hourly`, `* out hourly`) — InfluxDB |
| 112 | + continuous queries / downsampling, not raw registers. |
| 113 | + |
| 114 | +## lux-mon parity gaps (to close) |
| 115 | + |
| 116 | +1. Derive `pv1_current` / `pv2_current` from `pv1_power / pv1_voltage` (and |
| 117 | + same for string 2) to match SA's "PV current 1/2". |
| 118 | +2. Add weather-sourced `outside_temperature`, `cloud_cover`, and |
| 119 | + `pv_power_predicted` (optional; requires a weather API). |
| 120 | +3. Add hourly downsampling measurements to match SA's `* hourly` series. |
| 121 | +4. (Optional) Add `CPU temperature` and `Free storage` for host health parity. |
0 commit comments