From 5484bb5fa6203d06371332181e5bbeae70db1dd3 Mon Sep 17 00:00:00 2001 From: noise Date: Tue, 23 Jun 2026 18:49:47 -0400 Subject: [PATCH] commissioned --- LVX6048/homeassistant/README.md | 19 ++++ .../homeassistant/daily_energy_package.yaml | 88 ++++++++++++++++++ .../__pycache__/eg4-batterycpython-311.pyc | Bin 0 -> 65356 bytes eg4battery/bin/eg4-battery | 67 +++++++++++++ 4 files changed, 174 insertions(+) create mode 100644 LVX6048/homeassistant/daily_energy_package.yaml create mode 100644 eg4battery/bin/__pycache__/eg4-batterycpython-311.pyc diff --git a/LVX6048/homeassistant/README.md b/LVX6048/homeassistant/README.md index 675e0c2..867e5a3 100644 --- a/LVX6048/homeassistant/README.md +++ b/LVX6048/homeassistant/README.md @@ -12,6 +12,7 @@ Mirrors the `eg4battery/homeassistant/` pattern. |----------------------------|--------------------------------------------------------------| | `mqtt_controls.yaml` | `configuration.yaml` → `mqtt: !include lvx6048/mqtt_controls.yaml` (or merge by hand) | | `template_sensors.yaml` | `configuration.yaml` → `template: !include lvx6048/template_sensors.yaml` | +| `daily_energy_package.yaml`| `configuration.yaml` → `homeassistant: packages: {lvx6048_daily_energy: !include lvx6048/daily_energy_package.yaml}` | | `lovelace_controls.yaml` | Raw Lovelace card config — paste into a new dashboard view | The auto-discovery sensors (battery V, fault code, mode, MPPT power, …) @@ -103,6 +104,24 @@ mosquitto_sub -h -u mqtt -P -v \ …then flip a select in the dashboard. Both inverters should publish `"Succeeded"` within ~1 s. +## Daily energy indicators (harvested / used today) + +`daily_energy_package.yaml` adds two daily-resetting kWh sensors for a +dashboard tile: + +- `sensor.solar_harvested_today` — PV generated today, summed across both + inverters. Sourced from each inverter's lifetime `total_pv_generated_energy` + Wh counter via a daily `utility_meter` (the LVX6048 firmware has no + daily-energy query, so we meter the lifetime counter instead). +- `sensor.solar_used_today` — household load consumed today. No cumulative + load counter exists, so `sensor.lvx6048_stack_ac_output_active_power` (W) + is Riemann-integrated into energy and metered daily. + +Requires `template_sensors.yaml` (for the stack load-power rollup). A ready +two-tile Lovelace card is in the file's trailing comment. After enabling, +the two sensors read 0 at local midnight and climb through the day; give the +integration a few minutes to accumulate its first non-zero value. + ## Energy / SoC dashboard wiring (optional) Once both inverters' `ac_output_active_power` and the EG4 daemon's diff --git a/LVX6048/homeassistant/daily_energy_package.yaml b/LVX6048/homeassistant/daily_energy_package.yaml new file mode 100644 index 0000000..a17dcc6 --- /dev/null +++ b/LVX6048/homeassistant/daily_energy_package.yaml @@ -0,0 +1,88 @@ +# Today's solar harvested + household energy used — daily-resetting kWh +# sensors for a dashboard indicator. +# +# This is a Home Assistant *package* (bundles several domains in one file) +# so the whole feature installs as one unit. +# +# Install: +# 1. Copy to /lvx6048/daily_energy_package.yaml +# 2. In configuration.yaml (once), pull it in as a package: +# homeassistant: +# packages: +# lvx6048_daily_energy: !include lvx6048/daily_energy_package.yaml +# 3. Requires template_sensors.yaml already loaded (provides +# sensor.lvx6048_stack_ac_output_active_power). +# 4. Restart HA. +# +# Produces two dashboard-ready entities: +# sensor.solar_harvested_today kWh — PV generated today (both inverters) +# sensor.solar_used_today kWh — household load consumed today +# +# Data sources & why: +# - Harvested: each inverter exposes a *lifetime* Wh counter +# (total_pv_generated_energy); the LVX6048 firmware has no daily-energy +# query (ED is NAKed). A daily utility_meter on the lifetime counter gives +# today's harvest from the inverter's own internal accumulator (accurate). +# - Used: no cumulative load counter exists, so the household load *power* +# (sensor.lvx6048_stack_ac_output_active_power, sum of both inverters' +# AC output from template_sensors.yaml) is Riemann-integrated into energy +# and metered daily. This is an estimate; accuracy tracks powermon's GS +# poll cadence. Counts only load served by the inverters. +# +# NOTE: verify the two source entity_ids match your HA install +# (Developer Tools -> States): sensor.lvx6048_1_total_pv_generated_energy +# and sensor.lvx6048_2_total_pv_generated_energy. + +template: + - sensor: + - name: "Solar Harvested Today" + unique_id: solar_harvested_today + unit_of_measurement: "kWh" + device_class: energy + state_class: total_increasing + icon: mdi:solar-power + availability: > + {{ has_value('sensor.solar_pv_today_unit_1') + and has_value('sensor.solar_pv_today_unit_2') }} + state: > + {{ ((states('sensor.solar_pv_today_unit_1') | float(0)) + + (states('sensor.solar_pv_today_unit_2') | float(0))) / 1000 }} + +# Riemann-sum integration: household load power (W) -> energy (kWh) +sensor: + - platform: integration + source: sensor.lvx6048_stack_ac_output_active_power + name: "Household Load Energy" + unique_id: household_load_energy_total + unit_prefix: k + round: 3 + method: trapezoidal + +# Daily-resetting meters (reset at local midnight) +utility_meter: + solar_pv_today_unit_1: + source: sensor.lvx6048_1_total_pv_generated_energy + cycle: daily + solar_pv_today_unit_2: + source: sensor.lvx6048_2_total_pv_generated_energy + cycle: daily + solar_used_today: + source: sensor.household_load_energy + cycle: daily + +# --------------------------------------------------------------------------- +# Dashboard card (paste into a Lovelace view, or Raw config editor): +# +# type: horizontal-stack +# cards: +# - type: tile +# entity: sensor.solar_harvested_today +# name: Harvested Today +# icon: mdi:solar-power +# color: amber +# - type: tile +# entity: sensor.solar_used_today +# name: Used Today +# icon: mdi:home-lightning-bolt +# color: blue +# --------------------------------------------------------------------------- diff --git a/eg4battery/bin/__pycache__/eg4-batterycpython-311.pyc b/eg4battery/bin/__pycache__/eg4-batterycpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0ed9eca0258e5e0d16265568002cb82a2ade9719 GIT binary patch literal 65356 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 AppConfig: @@ -128,6 +129,7 @@ def load_config(path: Path) -> AppConfig: packs=[PackConfig(**p) for p in raw["packs"]], cell_count=raw.get("cell_count", 16), expose_raw_registers=raw.get("expose_raw_registers", False), + soc_estimator=raw.get("soc_estimator", False), ) @@ -733,6 +735,9 @@ _FIELD_META.update({ "model": (None, None, None, "mdi:battery-outline"), "firmware_version": (None, None, None, "mdi:chip"), "firmware_date": (None, None, None, "mdi:calendar"), + # independent coulomb-count SoC estimator (opt-in; see estimate_soc) + "soc_estimated": ("%", "battery", "measurement", "mdi:battery-sync"), + "soc_est_anchor": (None, None, None, "mdi:anchor"), }) def field_meta(key: str) -> tuple[str | None, str | None, str | None, str | None]: if key.startswith("register_"): @@ -759,6 +764,7 @@ _FIELD_PRECISION: dict[str, int] = { "capacity_ah": 1, "remaining_ah": 1, "error_code": 0, + "soc_estimated": 1, } for _i in range(1, 17): _FIELD_PRECISION[f"cell_{_i:02d}_voltage"] = 3 @@ -863,11 +869,70 @@ class _PackState: consecutive_errors: int = 0 response_count: int = 0 first_seen_logged: bool = False + # independent SoC estimator (opt-in; see estimate_soc) + soc_est: float | None = None + last_est_ts: float | None = None _FAIL_HEARTBEAT_CYCLES = 360 # re-log a stuck failure every ~hour at 10 s cadence +# --- independent SoC estimator (coulomb count + voltage anchoring) ---------- +# The BMS reg21 SoC tracks current well (validated 2026-06-15: reported dSoC +# matched the integral of pack_current to <1%/hr across all 6 packs) but only +# re-anchors at a full-charge termination the bank rarely reaches while it +# cycles mid-range — so the six counters free-run-drift apart (observed +# 29-60% at an identical 3.33 V/cell). This estimator integrates the same +# trusted current but adds the anchors the BMS lacks: snap to 100% at a +# detected full charge, snap to a low ref at the bottom knee. Published as a +# separate `soc_estimated` entity; it never replaces the raw `soc`. Pure +# function of (readings, prior per-pack state, now) so it can be replayed +# offline against captured current logs. +SOC_EST_CFG = dict( + capacity_ah=100.0, # LP4V2 nameplate (per-pack capacity_ah reg reads 100.0) + coulombic_eff=0.99, # charge-acceptance efficiency, applied to + current + v_full=3.450, # cell Vmax at/above this ... + i_taper=3.0, # ... while charge current has tapered below this (A) => full + soc_full=100.0, + v_empty=3.000, # cell Vmin at/below this (under load) ... + soc_empty=5.0, # ... => bottom anchor + max_gap_s=300.0, # ignore integration across gaps longer than this (restarts) +) + + +def estimate_soc(readings: dict[str, Any], st: "_PackState", now: float, + cfg: dict = SOC_EST_CFG) -> None: + """Add `soc_estimated` + `soc_est_anchor` to `readings`, coulomb-counting + pack_current between cycles and re-anchoring at full / empty. No-op for the + cycle if pack_current is missing (can't integrate).""" + cur = readings.get("pack_current") + if cur is None: + return + vmax = readings.get("cell_voltage_max") + vmin = readings.get("cell_voltage_min") + + if st.soc_est is None: # seed from the BMS on first sight + bms = readings.get("soc") + st.soc_est = float(bms) if bms is not None else 50.0 + st.last_est_ts = now + + dt = 0.0 if st.last_est_ts is None else min(now - st.last_est_ts, cfg["max_gap_s"]) + st.last_est_ts = now + if dt > 0: # integrate charge (Ah) -> % of pack + eff = cfg["coulombic_eff"] if cur > 0 else 1.0 + st.soc_est += (cur * eff * dt / 3600.0) / cfg["capacity_ah"] * 100.0 + + anchor = "coulomb" # anchors override the free-run integral + if vmax is not None and 0.0 <= cur < cfg["i_taper"] and vmax >= cfg["v_full"]: + st.soc_est, anchor = cfg["soc_full"], "full" + elif vmin is not None and vmin <= cfg["v_empty"]: + st.soc_est, anchor = cfg["soc_empty"], "empty" + + st.soc_est = max(0.0, min(100.0, st.soc_est)) + readings["soc_estimated"] = round(st.soc_est, 1) + readings["soc_est_anchor"] = anchor + + def _resolve_pack_name(addr: int, packs: list[PackConfig]) -> str: for p in packs: if p.address == addr: @@ -996,6 +1061,8 @@ def run_modbus_per_pack(cfg: AppConfig, publisher: MQTTPublisher, raise RuntimeError(f"no poller configured for {p.name}") regs = pollers[p.name].poll() readings = decode_eg4_modbus_regs(regs, expose_raw=cfg.expose_raw_registers) + if cfg.soc_estimator: + estimate_soc(readings, st, time.monotonic()) publisher.publish_pack(p.name, readings) st.response_count += 1 if not st.ok and st.consecutive_errors > 0: