updates to evse

This commit is contained in:
2026-05-09 11:34:26 -04:00
parent 8c7b5fb711
commit df3fb3466a
3 changed files with 75 additions and 26 deletions

View File

@@ -11,6 +11,7 @@ Mirrors the `eg4battery/homeassistant/` pattern.
| File | Where it goes in HA |
|----------------------------|--------------------------------------------------------------|
| `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` |
| `lovelace_controls.yaml` | Raw Lovelace card config — paste into a new dashboard view |
The auto-discovery sensors (battery V, fault code, mode, MPPT power, …)
@@ -20,6 +21,10 @@ This folder only adds the pieces HA can't infer:
- **Control entities** — selects + numbers that publish to
`solar/control/lvx6048/<action>` so users can change settings from a
dashboard without touching the LCD.
- **Stack-total derived sensors** — the PI18 GS command only exposes per-unit
numbers, so a 240 V load split across two LVX6048s shows as ~half on each.
`template_sensors.yaml` synthesizes household-level totals (apparent_power,
active_power, mppt1_input_power).
- **A dashboard view** that wraps those controls with the existing
telemetry into one screen.

View File

@@ -0,0 +1,54 @@
# Derived template sensors for the 2× LVX6048 parallel stack.
# Each LVX6048 reports its own AC output and MPPT contribution; PI18 GS has
# no system-total field (PGS does, but powermon doesn't poll it). These
# rollups synthesize the household-level numbers from the per-unit data.
#
# Include into configuration.yaml as:
# template: !include lvx6048/template_sensors.yaml
#
# Stack rollups created:
# sensor.lvx6048_stack_ac_output_apparent_power VA (unit_1 + unit_2)
# sensor.lvx6048_stack_ac_output_active_power W (unit_1 + unit_2)
# sensor.lvx6048_stack_mppt1_input_power W (solar in, both units)
#
# Background: with the inverters in 120/240 split-phase wiring, an EV
# pulling 24 A at 240 V drives 24 A through each leg, so each LVX shows
# only its 120 V × 24 A ≈ 2880 VA contribution. The stack rollup is the
# real household-level number.
- sensor:
- name: "lvx6048_stack ac_output_apparent_power"
unique_id: lvx6048_stack_ac_output_apparent_power
unit_of_measurement: "VA"
device_class: apparent_power
state_class: measurement
state: >
{% set p = [
states('sensor.lvx6048_1_ac_output_apparent_power') | float(0),
states('sensor.lvx6048_2_ac_output_apparent_power') | float(0),
] %}
{{ p | sum | round(0) }}
- name: "lvx6048_stack ac_output_active_power"
unique_id: lvx6048_stack_ac_output_active_power
unit_of_measurement: "W"
device_class: power
state_class: measurement
state: >
{% set p = [
states('sensor.lvx6048_1_ac_output_active_power') | float(0),
states('sensor.lvx6048_2_ac_output_active_power') | float(0),
] %}
{{ p | sum | round(0) }}
- name: "lvx6048_stack mppt1_input_power"
unique_id: lvx6048_stack_mppt1_input_power
unit_of_measurement: "W"
device_class: power
state_class: measurement
state: >
{% set p = [
states('sensor.lvx6048_1_mppt1_input_power') | float(0),
states('sensor.lvx6048_2_mppt1_input_power') | float(0),
] %}
{{ p | sum | round(0) }}

View File

@@ -292,19 +292,18 @@ def decode_modbus_response(frame: ModbusFrame) -> dict[str, Any]:
# live probing of a single pack. See ../NOTES.md "Register map" section.
# High-confidence fields promoted to named entities; unknowns (reg 32, 35,
# 38-40, 43-45) still emitted as register_NN for correlation.
_WARNING_BITS = [
"pack_ov", "cell_ov", "pack_uv", "cell_uv",
"charge_oc", "discharge_oc", "temp_anomaly", "mos_ot",
"charge_ot", "discharge_ot", "charge_ut", "discharge_ut",
"low_capacity", "other_error",
]
_PROTECTION_BITS = [
"pack_ov", "cell_ov", "pack_uv", "cell_uv",
"charge_oc", "discharge_oc", "temp_anomaly", "mos_ot",
"charge_ot", "discharge_ot", "charge_ut", "discharge_ut",
"float_stopped", "discharge_sc",
]
#
# regs 33 and 34 USED to decode as warning_* / protection_* bitfields here,
# per the lv_host.app UI display order. That mapping was wrong: live probe
# (2026-05-09, all 3 packs) showed reg33 == reg34 always, with both halves
# of each register equal to the byte value of reg20 (temperature_03):
# pack 1/2 temp_03 = 19 (0x13) → reg33 = reg34 = 0x1313
# pack 3 temp_03 = 18 (0x12) → reg33 = reg34 = 0x1212
# i.e. the "alarms that flipped between packs" were just a 1 °C ambient
# difference, decoded as if the temperature byte were a 14-bit alarm mask.
# The real alarm word is reg35 (error_code), already exposed and = 0 on
# healthy packs. Don't re-add reg33/34 as alarms without first confirming
# they break out of the temp-mirror pattern under a real fault condition.
def _signed16(v: int) -> int:
@@ -357,13 +356,10 @@ def decode_eg4_modbus_regs(regs: list[int], expose_raw: bool = False) -> dict[st
# --- max charge/discharge current limit (reg 31), A ---
out["max_current_limit"] = round(regs[31] / 100.0, 2)
# --- bitfields: warnings (reg 33), protections (reg 34), error code (reg 35) ---
warn = regs[33]
for i, name in enumerate(_WARNING_BITS):
out[f"warning_{name}"] = "on" if (warn >> i) & 1 else "off"
prot = regs[34]
for i, name in enumerate(_PROTECTION_BITS):
out[f"protection_{name}"] = "on" if (prot >> i) & 1 else "off"
# --- error / alarm word (reg 35) ---
# reg 33/34 used to decode as warning_/protection_ bitfields but were
# actually mirroring a temperature byte — see the file-header comment.
# reg 35 is the canonical alarm word; 0 on healthy packs.
out["error_code"] = regs[35]
# --- static-ish (regs 36, 37) ---
@@ -706,12 +702,6 @@ _FIELD_META.update({
"firmware_version": (None, None, None, "mdi:chip"),
"firmware_date": (None, None, None, "mdi:calendar"),
})
for _name in _WARNING_BITS:
_FIELD_META[f"warning_{_name}"] = (None, None, None, "mdi:alert")
for _name in _PROTECTION_BITS:
_FIELD_META[f"protection_{_name}"] = (None, None, None, "mdi:shield-alert")
def field_meta(key: str) -> tuple[str | None, str | None, str | None, str | None]:
if key.startswith("register_"):
return (None, None, "measurement", "mdi:numeric")