updates to evse
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
54
LVX6048/homeassistant/template_sensors.yaml
Normal file
54
LVX6048/homeassistant/template_sensors.yaml
Normal 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) }}
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user