Skip to content

Commit cd622f6

Browse files
Antonio ...claude
andcommitted
feat: v2.13.0 — off-grid amperage ceiling (#57) + file-logging and SOC-field fixes (#59, #58)
#57 — Solar Surplus had zero behaviour difference between "grid present" and "grid confirmed lost", so on an islanded hybrid inverter it kept sizing amperage from PV surplus alone until the inverter tripped an AC over-current fault. New opt-in number `evsc_offgrid_max_amperage` (default 32 = off) is a second, stricter ceiling applied ONLY while the optional `grid_available` sensor reads explicitly off, so full amperage stays available every day the grid is up. Gated on the fail-safe `is_grid_available()` (None when unmapped or unavailable), snapped down to a valid amp level, composed with the existing solar-max cap (stricter wins), plus a hard single-step clamp of a session already running over the ceiling, evaluated before every other gate. Entity counts 73/59 -> 74/60. #59 — The daily file-logging handler is attached to the package logger, but nothing set that logger's level, so installs with `logger: default: warning` dropped every EVSC record before it reached the handler: the toggle created the file and wrote nothing. Enabling file logging now lowers the package logger to INFO (idempotent, rotation-safe, never narrows an explicit DEBUG) and restores the previous level on disable. `evsc_log_file_path` exposes `file_logging_active` and `effective_log_level`. #58 — The SOC fields were sensor-only selectors, forcing users whose EV exposes no SOC to wrap an `input_number` in a template sensor. They now accept sensor/number/input_number; every reader already goes through the domain-agnostic state_helper. Full suite green: 359 passed / 0 failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent c00fe0e commit cd622f6

19 files changed

Lines changed: 651 additions & 10 deletions

‎CLAUDE.md‎

Lines changed: 126 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
77
This is a **Home Assistant custom integration** for intelligent EV charging control. It manages EV charger automation based on solar production, time of day, battery levels, grid import protection, and intelligent priority balancing between EV and home battery charging.
88

99
**Domain:** `ev_smart_charger`
10-
**Current Version:** 2.12.1
10+
**Current Version:** 2.13.0
1111
**Installation:** HACS custom repository or manual installation to `custom_components/ev_smart_charger`
1212

1313
## Development Commands
@@ -757,6 +757,131 @@ async def _set_amperage(self, target_amperage: int):
757757

758758
## Version History
759759

760+
### v2.13.0 (2026-09-07)
761+
**FEATURE: opt-in off-grid amperage ceiling for Solar Surplus (issue #57) + two
762+
fixes: empty file-logging output (issue #59) and SOC fields that rejected a
763+
plain helper (issue #58)**
764+
765+
**#57 — off-grid amperage ceiling (feature, opt-in).** Solar Surplus sizes
766+
amperage purely from PV surplus (plus battery support) and had **zero behaviour
767+
difference between "grid present" and "grid confirmed lost"** — a grep for any
768+
islanding awareness in `solar_surplus.py` returned nothing. On a hybrid inverter
769+
running islanded, the real per-phase AC-output capacity can be far below what
770+
abundant PV plus a full home battery supplies, so EV charging plus house load
771+
can exceed the inverter's AC-output protection. The reporter's Deye tripped
772+
**F33 `AC_OverCurr_Fault`** twice on 2026-08-20 while fully islanded, with the
773+
EV at ~18–19 A. The existing `evsc_solar_max_amperage` is too blunt a tool: it
774+
caps unconditionally, so preventing a recurrence meant sacrificing charging
775+
speed every single day the grid is up.
776+
777+
New always-created number **`evsc_offgrid_max_amperage`** ("Off-Grid Max
778+
Amperage", `mdi:transmission-tower-off`, 6–32, default **32 = off**, mirroring
779+
`evsc_solar_max_amperage`). Not battery-only — off-grid capacity is a
780+
wiring/inverter fact independent of a mapped home battery. Entity counts
781+
73→**74**, 59→**60**. On generic chargers the existing step-rewrite block
782+
(step=1 for unit "A") applies automatically.
783+
784+
The gate reuses the already production-proven v2.6.0 signal:
785+
`ChargingModel.is_grid_available()` returns `None` when `grid_available` is
786+
unmapped **or** reads unavailable/unknown, and `True`/`False` only on an
787+
explicit on/off. `_get_offgrid_cap_amps()` returns a cap **only** when that
788+
answer is exactly `False` *and* the configured ceiling is below the top amp
789+
level, snapping down to the highest valid level (17 A → 16 A) so a non-standard
790+
amperage is never sent to the wallbox, and clamping at the 6 A floor rather than
791+
an invalid 0. So a boot-time flap, an inverter-integration restart, or an
792+
unmapped sensor can never throttle charging — and an install that leaves the
793+
default sees byte-for-byte v2.12.x behaviour.
794+
795+
Two injection points in `_async_periodic_check`:
796+
- **Target clamp**, immediately after the existing solar-max cap. Both caps run
797+
in sequence on the same `target_amps`, so whichever is stricter simply wins —
798+
no composition logic.
799+
- **`_offgrid_overload_guard()`**, evaluated at the top of "Apply Charging
800+
Logic" *before* Grid Import Protection, so a genuine off-grid overload is
801+
corrected first. Deliberately a **hard, single-step** `set_amperage()` clamp
802+
rather than the gradual one-level-per-tick surplus walk-down: the point is
803+
protecting the inverter now, not several minutes from now.
804+
`set_amperage()` already runs the safe stop → set → start sequence when
805+
decreasing on Tuya-model chargers, so it is safe on every supported wallbox.
806+
Extracted as its own method (returns True when it acted, caller aborts the
807+
tick) rather than inlined, so the gate is unit-testable.
808+
809+
**No debounce**, by design: `is_grid_available()` *is* the debounced fail-safe
810+
signal, and a reversible amperage cap does not need the anti-flap protection a
811+
*terminal* stop (Night Charge's `STOP_REASON_GRID_LOSS`) needs — if the grid
812+
returns, the cap stops applying on the next tick and the normal surplus ramp
813+
resumes on its own. **Scope: Solar Surplus only** — Night Smart Charge already
814+
stops terminally on grid loss. `offgrid_cap_a` is added to the Solar Surplus
815+
diagnostic payload, and a dedicated `OFFGRID_OVERLOAD_PROTECTION` diagnostic
816+
state is emitted when the guard fires.
817+
818+
**#59 — file logging produced an EMPTY daily file (bug).** Reported as a
819+
side-effect while diagnosing a charging-current question: *"I've enabled logging
820+
in EV Smart Charger but nothing is currently getting logged."* Root cause: the
821+
daily `FileHandler` is attached to the `custom_components.ev_smart_charger`
822+
package logger, but **a handler only ever receives records the logger itself
823+
lets through**. Nothing anywhere set the package logger's level, so on any
824+
install whose `logger:` block sets `default: warning` (or stricter — a common
825+
setup, and the shape this project's own README suggests) every EVSC `INFO`
826+
record was dropped at the `isEnabledFor` check. The toggle created the file and
827+
then wrote nothing to it, which is indistinguishable from a broken feature.
828+
829+
`EVSCLogger.enable_global_file_logging()` now calls
830+
`_ensure_package_level_for_file_logging()` (under the existing handler lock):
831+
when the effective level is above INFO it remembers the previous *explicit*
832+
level and lowers the package logger to INFO; `disable_global_file_logging()`
833+
restores it. Idempotent, so the midnight rotation's disable→enable pair and any
834+
repeated enable cannot corrupt the saved level, and a user who explicitly asked
835+
for `DEBUG` is never narrowed to INFO. **INFO, not DEBUG**, is deliberate: it is
836+
the activity stream the daily log is documented to contain, it matches Home
837+
Assistant's own default root level (so most installs see no change in
838+
`home-assistant.log`), and DEBUG would flood it — users who want DEBUG in the
839+
file still set it in `logger:` and the handler picks up whatever the logger
840+
passes. `sensor.evsc_log_file_path` gained `file_logging_active` and
841+
`effective_log_level` attributes so an empty file is self-diagnosable.
842+
843+
**#58 — SOC fields rejected a plain helper (fix, UI affordance).** Reported by a
844+
user whose EV exposes no SOC at all; the community workaround was to wrap an
845+
`input_number` in a template sensor purely to satisfy a sensor-only selector.
846+
EV SOC stays **architecturally required** — the Priority Balancer's daily
847+
targets, Night Smart Charge's stop conditions and Boost's target all key off it,
848+
so a genuinely SOC-less mode is not a small change — but there is no reason to
849+
force a wrapper: every reader goes through `state_helper`, which is
850+
domain-agnostic. New `SOC_INPUT_DOMAINS = ["sensor", "number", "input_number"]`
851+
is now the selector for both `soc_car` and `soc_home` in every flow, so an
852+
`input_number` helper can be mapped directly. No schema/migration change —
853+
existing entries keep their mapped sensor.
854+
855+
**Files**: `const.py` (`HELPER_OFFGRID_MAX_AMPERAGE_SUFFIX`,
856+
`DEFAULT_OFFGRID_MAX_AMPERAGE`, `SOC_INPUT_DOMAINS`, counts, VERSION),
857+
`number.py` (+1 always-created number), `solar_surplus.py` (discovery,
858+
`_get_offgrid_cap_amps`, `_offgrid_overload_guard`, target clamp, diagnostic),
859+
`config_flow.py` (#58), `utils/logging_helper.py` (#59), `sensor.py` (#59
860+
attributes), `strings.json` + `translations/{en,it,nl}.json`,
861+
`frontend/ev-smart-charger-dashboard.js` (suffix map + Solar settings stepper
862+
EN/IT/NL), `README.md`, `manifest.json`; tests: NEW
863+
`tests/test_v2130_offgrid_cap.py` (12: fail-safe when unmapped /
864+
unavailable / unknown, no-op when grid present, no-op at default 32 A, cap
865+
applies off-grid, snap-down to a valid level, never below the 6 A floor,
866+
immediate clamp of a running over-ceiling session, no spurious call at/below
867+
the ceiling, no-op when the charger is off, no-op when the grid is present),
868+
NEW `tests/test_v2130_file_logging_level.py` (6: level lowered, records really
869+
reach the file, previous level restored, explicit DEBUG never narrowed,
870+
rotation-safe saved level, level exposed for diagnostics), NEW
871+
`tests/test_v2130_soc_input_domains.py` (1: the constant *and* the config-flow
872+
selector offer the helper domains), plus entity-count updates in
873+
`test_config_flow.py` / `test_entity_platforms.py`. `VERSION = "2.13.0"`.
874+
No config-flow schema change (step counts unchanged). Full suite green:
875+
**359 passed / 0 failed**.
876+
877+
**Upgrade priority**: 🟢 RECOMMENDED for hybrid-inverter owners who ever run
878+
islanded — map `grid_available` and set the ceiling to your inverter's real
879+
per-phase off-grid capacity (default 32 A = off, so nothing changes until you
880+
do). 🟢 RECOMMENDED for anyone who has ever enabled file logging and found the
881+
file empty. ⚪ NO-OP otherwise.
882+
883+
---
884+
760885
### v2.12.1 (2026-08-31)
761886
**FIX: the EV charging Live Activity stayed on "charging" after the charge was interrupted**
762887

‎README.md‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,8 @@ The charger switch entity is used as the unique ID for the config entry. Adding
434434
| EV charging power (v2.2.0) | No | `W` |
435435
| Grid available (v2.6.0) | No | `binary_sensor` |
436436

437+
**EV battery SOC (v2.13.0 — [issue #58](https://github.com/antbald/ha-ev-smart-charger/issues/58)):** the two SOC fields accept a `sensor`, a `number` **or an `input_number`**. EV SOC is architecturally required — the Priority Balancer's daily targets, Night Smart Charge's stop conditions and Boost's target all key off it — but many EVs expose no SOC at all over any Home Assistant integration. Those owners can create an `input_number` helper, map it directly here, and keep it up to date manually or from their own automation. Before v2.13.0 the field was a sensor-only selector, forcing a template-sensor wrapper around the helper.
438+
437439
**EV charging power (v2.2.0):** the most reliable signal for whether the car is actually drawing current — when mapped it becomes the single source of truth for charging detection (and the dashboard's green "EV charging" banner), overriding the status string. In three-phase mode map all three per-phase sensors (they are summed) or leave all blank. The sensor must report positive watts while charging; if yours reports negative, wrap it in a template sensor (`{{ states('sensor.your_power') | float(0) | abs }}`) — the diagnostic sensor's `charging_power_w` will read a flat 0 W if the sign is reversed.
438440

439441
**Battery power (v2.1.0):** optional signed power sensor for the home battery, used by Hybrid Inverter Mode and Night Smart Charge grid mode to tell real solar headroom apart from the home battery silently covering the EV. **Sign convention: NEGATIVE = discharging, POSITIVE = charging.** If your sensor reports the opposite sign, invert it with a template sensor that negates the value, e.g. `{{ states('sensor.your_battery_power') | float(0) * -1 }}` — the diagnostic sensor's `battery_discharge_w` will read a flat 0 W if the sign is reversed.
@@ -567,6 +569,7 @@ When a day's flag is **ON** and the EV target is not yet reached at sunrise, Nig
567569
| `evsc_surplus_drop_delay` | `30` | `0–120` | s | How long surplus must be insufficient before stopping the charger. |
568570
| `evsc_spike_response_delay` | `10` | `0–60` | s | Consumption-spike fast response (v2.8.0): with stable PV, after this many seconds of grid import the charger steps down in one operation to the level that zeroes the import. `0` disables (legacy slow ramp). |
569571
| `evsc_solar_max_amperage` | `32` | `6–32` | A | Hard ceiling on Solar Surplus amperage. Lower this if your wallbox rejects currents above a certain value (e.g. set to `16` for wallboxes limited to 16 A). |
572+
| `evsc_offgrid_max_amperage` | `32` | `6–32` | A | Stricter ceiling applied **only while the optional `grid_available` sensor reads off** (hybrid inverter islanded). `32` = disabled. Requires `grid_available` to be mapped; see [Off-grid amperage ceiling](#off-grid-amperage-ceiling). |
570573
| `evsc_home_battery_min_soc` | `20` | `0–100` | % | Home battery must be above this SOC before battery support activates. |
571574
| `evsc_battery_support_amperage` | `16` | `6–32` | A | Amperage used when the home battery supplements solar charging. |
572575
| `evsc_battery_support_sunset_buffer` | `60` | `0–240` | min | Block home battery support when sunset is closer than this. Prevents draining the home battery in the last minutes of fading solar (e.g. plug-in at 18:00 with sunset at 19:15). Set to `0` to disable the guard. |
@@ -761,6 +764,32 @@ Battery support is also blocked when sunset is within `evsc_battery_support_suns
761764

762765
`evsc_solar_max_amperage` (default `32 A`) sets a hard ceiling so Solar Surplus never exceeds what your wallbox accepts. Set this to `16` if your wallbox rejects `20 A` or higher commands.
763766

767+
#### Off-grid amperage ceiling
768+
769+
On a hybrid inverter running **islanded** (grid lost), the real per-phase AC
770+
output capacity can be far below what abundant PV plus a full home battery
771+
would otherwise supply. Solar Surplus sizes amperage from surplus alone, so EV
772+
charging plus house load could exceed the inverter's AC-output protection and
773+
trip an over-current fault.
774+
775+
`evsc_offgrid_max_amperage` (default `32 A` = **off**) is a second, stricter
776+
ceiling that applies **only** while the optional `grid_available` binary sensor
777+
reads explicitly `off`:
778+
779+
- **Grid present, or the sensor unmapped / `unavailable` / `unknown` → the
780+
ceiling never applies.** This is deliberate and fail-safe: a boot-time or
781+
inverter-restart flap can never throttle your charging.
782+
- Both ceilings run in sequence on the same target, so whichever is stricter
783+
wins — you keep full amperage every day the grid is up and only give up
784+
capacity while genuinely islanded.
785+
- A session already running above the ceiling is clamped **immediately, in one
786+
step**, before any other gate — protecting the inverter now rather than
787+
walking down over several minutes.
788+
789+
Set it to the highest current your inverter can sustain per phase while
790+
islanded (e.g. a 12 kW three-phase unit ÷ 3 ≈ 4 kW/phase ≈ 17 A, de-rated in
791+
practice). Leave it at `32 A` to keep v2.12.x behaviour exactly.
792+
764793
---
765794

766795
### Priority Balancer
@@ -1210,6 +1239,18 @@ A new file is created automatically at midnight. Previous days are kept indefini
12101239

12111240
When troubleshooting is complete, disable the toggle to stop writing. Existing log files are preserved.
12121241

1242+
Turning the toggle ON also lowers the integration's own log level to `INFO` for
1243+
as long as file logging is active, and restores your previous level when you
1244+
turn it off. Before v2.13.0 an install whose `logger:` block set
1245+
`default: warning` (or stricter) dropped every EVSC record *before* it reached
1246+
the file handler, so the daily file was created and then stayed empty. If you
1247+
need `DEBUG` in the file, set it in the `logger:` block (see below) — the file
1248+
handler records whatever the logger passes.
1249+
1250+
The `evsc_log_file_path` sensor exposes `file_logging_active` and
1251+
`effective_log_level` as attributes, so an empty log file is diagnosable at a
1252+
glance.
1253+
12131254
### Trace Logging
12141255

12151256
Enable `evsc_trace_logging_enabled` for verbose output including every sensor read, every calculation step, and every state transition. Intended for short diagnostic sessions — disable immediately after use to avoid performance impact.

‎custom_components/ev_smart_charger/config_flow.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
CONF_PV_FORECAST,
3838
CONF_PV_FORECAST_TOMORROW,
3939
CONF_SOC_CAR,
40+
SOC_INPUT_DOMAINS,
4041
CONF_SOC_HOME,
4142
CHARGER_MODEL_GENERIC,
4243
CHARGER_MODEL_TUYA,
@@ -228,8 +229,8 @@ def _sensor_schema(
228229
soc_home_marker = vol.Required if existing_soc_home else vol.Optional
229230

230231
fields: dict[Any, Any] = {
231-
vol.Required(CONF_SOC_CAR, **_field_config(current_data.get(CONF_SOC_CAR))): _entity_selector("sensor"),
232-
soc_home_marker(CONF_SOC_HOME, **_field_config(existing_soc_home)): _entity_selector("sensor"),
232+
vol.Required(CONF_SOC_CAR, **_field_config(current_data.get(CONF_SOC_CAR))): _entity_selector(SOC_INPUT_DOMAINS),
233+
soc_home_marker(CONF_SOC_HOME, **_field_config(existing_soc_home)): _entity_selector(SOC_INPUT_DOMAINS),
233234
}
234235

235236
# Per-quantity power sensors, grouped L1[/L2/L3]. Single-phase = L1 only.

‎custom_components/ev_smart_charger/const.py‎

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
# ========== INTEGRATION METADATA ==========
44
DOMAIN = "ev_smart_charger"
5-
VERSION = "2.12.1"
5+
VERSION = "2.13.0"
66
DEFAULT_NAME = "EV Smart Charger"
77
FRONTEND_URL_BASE = "/api/ev_smart_charger/frontend"
88
FRONTEND_CARD_FILENAME = "ev-smart-charger-dashboard.js"
@@ -105,6 +105,11 @@
105105
CONF_EV_CHARGER_SWITCH = "ev_charger_switch"
106106
CONF_EV_CHARGER_CURRENT = "ev_charger_current"
107107
CONF_EV_CHARGER_STATUS = "ev_charger_status"
108+
# v2.13.0 (issue #58): SOC fields accept a plain helper as well as a sensor.
109+
# Many EVs expose no SOC at all; those users track it manually and had to wrap an
110+
# `input_number` in a template sensor purely to satisfy a sensor-only selector.
111+
# All readers go through state_helper, which is domain-agnostic.
112+
SOC_INPUT_DOMAINS = ["sensor", "number", "input_number"]
108113
CONF_SOC_CAR = "soc_car"
109114
CONF_SOC_HOME = "soc_home"
110115
CONF_FV_PRODUCTION = "fv_production"
@@ -270,6 +275,10 @@
270275
HELPER_BATTERY_SUPPORT_AMPERAGE_SUFFIX = "evsc_battery_support_amperage"
271276
HELPER_BATTERY_SUPPORT_SUNSET_BUFFER_SUFFIX = "evsc_battery_support_sunset_buffer"
272277
HELPER_SOLAR_MAX_AMPERAGE_SUFFIX = "evsc_solar_max_amperage"
278+
# v2.13.0 (issue #57) — per-phase amperage ceiling applied by Solar Surplus ONLY
279+
# while the optional `grid_available` binary_sensor reads explicitly off (the
280+
# inverter is islanded). Default 32 (top of CHARGER_AMP_LEVELS) = cap disabled.
281+
HELPER_OFFGRID_MAX_AMPERAGE_SUFFIX = "evsc_offgrid_max_amperage"
273282
# v2.1.0 (issue #29) — max home-battery discharge (W) allowed to cover the EV
274283
# charging floor (deadband buffer + Hybrid Mode masking checks). 0 = feature off.
275284
# Battery-only helper (meaningless without a home battery).
@@ -401,6 +410,7 @@
401410
DEFAULT_BATTERY_SUPPORT_AMPERAGE = 16 # amps (user configurable)
402411
DEFAULT_BATTERY_SUPPORT_SUNSET_BUFFER_MIN = 60 # minutes before sunset (block battery support when close to sunset)
403412
DEFAULT_SOLAR_MAX_AMPERAGE = 32 # amps (user configurable, default = no cap)
413+
DEFAULT_OFFGRID_MAX_AMPERAGE = 32 # amps (32 = off, top of CHARGER_AMP_LEVELS)
404414
DEFAULT_MAX_BATTERY_DISCHARGE_FOR_EV = 0 # watts (0 = feature off, current behaviour)
405415

406416
# ========== DEFAULT VALUES - PRIORITY BALANCER ==========
@@ -528,7 +538,8 @@
528538
# of entities actually created when nothing is disabled. If it drifts above
529539
# reality (cf. v1.6.20), a single user-disabled entity turns the tolerant
530540
# startup path back into a hard ConfigEntryNotReady. Keep this in sync.
531-
TOTAL_INTEGRATION_ENTITIES = 73
541+
# v2.13.0 adds 1 always-created number (evsc_offgrid_max_amperage) → 74.
542+
TOTAL_INTEGRATION_ENTITIES = 74
532543
# Verified count (v2.3.0): 53 entities when running in PV-only mode.
533544
# Unchanged in v2.1.0: the discharge number is battery-only (skipped in PV-only mode).
534545
# v2.3.0 (issue #32): evsc_night_pv_handoff_threshold is NOT battery-only → +1 → 53.
@@ -541,7 +552,8 @@
541552
# 3 numbers (home_battery_min_soc, battery_support_amperage, battery_support_sunset_buffer),
542553
# 7 daily home min SOC numbers (Monday–Sunday), 1 sensor (today_home_target).
543554
# Hybrid Mode entities are still created in PV-only mode but stay IDLE (requires soc_home).
544-
TOTAL_INTEGRATION_ENTITIES_NO_BATTERY = 59
555+
# v2.13.0 (issue #57): the off-grid amperage cap is NOT battery-only → +1 → 60.
556+
TOTAL_INTEGRATION_ENTITIES_NO_BATTERY = 60
545557

546558

547559
def has_home_battery(config: dict) -> bool:

0 commit comments

Comments
 (0)