12 KiB
name, description
| name | description |
|---|---|
| homeassistant-ev | Edit and debug EV charging automations for the Renault R4 E-Tech in Home Assistant. Covers automations.yaml, get_ev_battery.py, entity IDs, the Renault API capabilities, charge scheduling logic, pricing integration, and VictoriaMetrics session logging. Any changes made to the EV automations or scripts must also be reflected in this skill file. |
Home Assistant EV Charging Automations
Working directory: /home/jonas/homeassistant/
Key files
| File | Purpose |
|---|---|
automations.yaml |
All automations including the full EV charging suite |
get_ev_battery.py |
Two modes: poll current battery from Renault API; sync completed session data |
configuration.yaml |
Defines input_number, input_text, timer, shell_command helpers |
Architecture overview
The EV charger is a Shelly Pro 1PM smart switch (switch.shellypro1pm_8c4f00b426d8) that sits between the wall socket and the car's charging cable.
Important limitations of the Renault API:
battery-status.batteryLevelis arbitrarily stale (typically 10–30 min old)- The
timestampfield tells you when the car last reported, but there is no way to know the current battery level in real time sensor.shellypro1pm_8c4f00b426d8_powerdoes NOT reflect what the car draws — the car controls its own intake independently- There is no charge cap / target SOC API in renault-api for this model
Consequence: mid-session polling cannot give reliable trajectory data. The stop mechanism is purely time-based.
Stop mechanism: time-based timer with two-zone charging curve
AC home charging is not linear. The R4 E-Tech charges at roughly 10 kW below 80% SOC and tapers to around 3.8–5 kW above 80%. A single average rate would overestimate duration for sessions that don't cross 80% (causing early stop), and underestimate for sessions that do (causing overshoot).
Instead, duration is computed with a two-zone formula:
total_min = pct_below_80 * mpp_below + pct_above_80 * mpp_above
Where pct_below_80 = min(80, target) - start and pct_above_80 = max(0, target - 80).
The two min/% values are stored in:
input_number.ev_min_per_pct_below_80— EMA-fitted from sessions entirely or mostly below 80%input_number.ev_min_per_pct_above_80— derived from sessions that cross 80%, using the below-80 rate to split session time
Both are updated after every session via get_ev_battery.py --sync-sessions, which re-fits the model deterministically from all available sessions in the last 60 days (starting from neutral seeds, not stored values, so the result is always idempotent).
timer.ev_charge_session is set to this duration and is the primary stop signal. When it fires, the Shelly is turned off and a Renault cloud charge-stop is sent.
A single mid-session sanity check fires at the halfway point (timer.ev_charge_poll). If the API reading at that point already confirms ≥ target, it stops early and cancels the session timer. Otherwise it does nothing — the session timer continues.
Rate calibration: BMS session data
After the Shelly turns off, timer.ev_post_charge_sync starts a 30-minute delay. When it fires, get_ev_battery.py --sync-sessions queries:
renault-api charge sessions --from YYYY-MM-DD --to YYYY-MM-DD
This returns the car's BMS-recorded sessions with exact chargeStartDate, chargeEndDate, chargeStartBatteryLevel, chargeEndBatteryLevel, and chargeEnergyRecovered (kWh).
Duration is computed from the timestamps (not from the chargeDuration field, which has display issues). Rate = energy_kwh / duration_hours. This is updated into input_number.ev_charging_power_kw as a rolling average of the last 3 sessions and logged to VictoriaMetrics as ev_session.
Helper entities (defined in configuration.yaml)
| Entity | Type | Purpose |
|---|---|---|
input_number.ev_target_charge |
number (50–100, step 5) | Target SOC%, default 80 |
input_number.ev_charging_power_kw |
number (1–22, step 0.1) | Below-80% rate in kW implied by mpp_below (display/fallback) |
input_number.ev_min_per_pct_below_80 |
number (0.5–15, step 0.01) | Minutes per % SOC for charging below 80%; fitted from BMS sessions |
input_number.ev_min_per_pct_above_80 |
number (0.5–30, step 0.01) | Minutes per % SOC for charging above 80%; derived from crossing sessions |
input_number.ev_real_battery |
number (0–100) | Raw API battery % (updated by get_ev_battery.py poll) |
input_number.ev_charge_session_start_battery |
number | SOC% at session start |
input_text.ev_charge_hours |
text | JSON array of planned charge hours, e.g. [22, 0, 1] |
input_text.ev_charge_session_start_ts |
text | Unix timestamp of session start |
input_text.ev_charge_rates |
text | JSON array of last 3 BMS-measured rates in kW |
timer.ev_charge_session |
timer | Primary stop: fires after calculated charge duration |
timer.ev_charge_poll |
timer | One-shot sanity check at session midpoint |
timer.ev_post_charge_sync |
timer | 30-min post-session delay before querying BMS data |
Automation IDs and what they do
ev_calculate_charge_hours
- Trigger: Time
21:50:00 - Action: Reads spot prices (DK2, Energi Data Service), computes hours needed from battery/target/rate, picks cheapest N hours in the 22:00–05:00 window, writes to
input_text.ev_charge_hours. Logs detail (prices, selection) and creates a persistent notification.
ev_hourly_charge_control
- Triggers: Time at 22:00, 23:00, 00:00, 01:00, 02:00, 03:00, 04:00, 05:00
- Action: Turns Shelly ON if current hour is in the planned list AND
sensor.r4_e_tech_battery < target; OFF otherwise. Logs decision with reason.
ev_startup_recovery
- Trigger:
homeassistant.start - Conditions:
now().hour in [21,22,23,0,1,2,3,4,5]AND battery < target - Action: Waits 2 min, recalculates charge hours, immediately applies the Shelly decision. Recovers from HA crashes during the charge window.
ev_charge_started (alias: "EV: Start charge session")
- Trigger: Shelly →
on - Action: Polls API (raw battery →
ev_real_battery), records session start. Sets:timer.ev_charge_session→ full calculated duration (primary stop)timer.ev_charge_poll→ half duration (sanity check)
ev_charge_poll_check (alias: "EV: Mid-session sanity check")
- Trigger:
timer.ev_charge_pollfinished - Condition: Shelly still
on - Action: Polls API. If
ev_real_battery >= target: cancels session timer, stops Shelly + Renault stop. Otherwise: logs reading only, leaves session timer running.
ev_session_time_reached (alias: "EV: Session timer stop")
- Trigger:
timer.ev_charge_sessionfinished - Condition: Shelly still
on - Action: Turns off Shelly + presses
button.r4_e_tech_stop_charge. This is the primary stop path.
ev_charge_stopped (alias: "EV: Charge session ended")
- Trigger: Shelly →
off - Action: Cancels
timer.ev_charge_pollandtimer.ev_charge_session(whichever is still running). Startstimer.ev_post_charge_sync(30 min). Logs session start battery and duration.
ev_sync_session_data (alias: "EV: Post-charge session sync")
- Trigger:
timer.ev_post_charge_syncfinished - Action: Calls
shell_command.sync_ev_sessions→get_ev_battery.py --sync-sessions. Logs updated rate.
Shelly off (id 1768306419435)
- Trigger: Time
06:00:00 - Action: Turns off Shelly, resets
ev_charge_hoursto[], resetsev_target_chargeto 80.
Shelly entity IDs (do not change)
device_id: e18cd73e8b837834b77acf81eca52224
entity_id: 81ed4fbd80fee38a4817667cd8737748 (used in type: turn_on/off)
switch.shellypro1pm_8c4f00b426d8 (used in state conditions and logbook)
Renault entity IDs
sensor.r4_e_tech_battery — SOC% from HA integration (stale, for scheduling only)
binary_sensor.r4_e_tech_charging — whether car reports actively charging
button.r4_e_tech_stop_charge — sends charge-stop via Renault cloud
device_id: 5866f4d42ced21f6ce609e6b19d1ef65
Renault API capabilities
CLI: ~/.local/bin/renault-api
| Command | Status | Notes |
|---|---|---|
renault-api --json status |
✅ | Returns battery-status with timestamp, batteryLevel, chargingRemainingTime |
renault-api charge sessions --from DATE --to DATE |
✅ | BMS-recorded sessions with start/end SOC and kWh. Primary data source for rate calibration. |
renault-api charge stop |
⚠️ | Model A4E1VE undocumented; may fail if not actively charging |
renault-api charge mode |
❌ | Access forbidden |
renault-api charge schedule show/set |
❌/⚠️ | show forbidden; set exists but is time+duration only, no SOC target |
chargeDuration field display note: the tabulated CLI output shows this in a misleading format. Always compute duration from chargeStartDate - chargeEndDate timestamps instead.
get_ev_battery.py — two modes
Poll mode (default):
- Calls
renault-api --json status, extracts rawbatteryLevelandtimestamp - Updates
input_number.ev_real_batterywith the raw API value (no projection) - Logs to VM:
ev_chargingmeasurement withbattery_api_pct,api_age_seconds,target_pct,api_timestamp_unix
Sync mode (--sync-sessions):
- Calls
renault-api charge sessions --from <60 days ago> --to <today> - Parses tabulated output: timestamps, start/end SOC%, energy kWh. Duration is always computed from
chargeStartDate–chargeEndDate(thechargeDurationfield has display issues). - Fits the two-zone model via chronological EMA starting from neutral seeds (3.0, 6.0):
- Sessions entirely ≤80%: directly update
mpp_below - Sessions crossing 80%: split time using current
mpp_below, derivempp_abovefrom the above-80% portion - Sessions entirely ≥80%: directly update
mpp_above
- Sessions entirely ≤80%: directly update
- Discards sessions with
delta_pct < 3orduration < 2 minas noise - Updates
input_number.ev_min_per_pct_below_80andinput_number.ev_min_per_pct_above_80 - Updates
input_number.ev_charging_power_kw(implied kW from mpp_below, for display) - Logs to VM:
ev_sessionmeasurement with start/end %, duration, energy, rate, and new mpp values
Note on crossing sessions with tiny above-80 range (e.g. 72→81%): the 1% above 80% amplifies timing noise into large mpp estimates. These sessions are processed but have low signal for mpp_above. Sessions with pct_above ≥ 5% are more reliable.
VictoriaMetrics data
VM at http://127.0.0.1:8428. Written by get_ev_battery.py via InfluxDB line protocol.
| Measurement | When written | Key fields |
|---|---|---|
ev_charging |
Each poll (session start, sanity check) | battery_api_pct, api_age_seconds |
ev_session |
30 min after session end | start_battery_pct, end_battery_pct, duration_minutes, energy_kwh, rate_kw |
HA influxdb integration also passively logs sensor.r4_e_tech_battery, input_number.ev_real_battery, input_number.ev_charging_power_kw on state change.
Debugging
Check logbook:
podman logs homeassistant 2>&1 | grep "EV Charge Control"
Run scripts manually:
python3 /home/jonas/homeassistant/get_ev_battery.py
python3 /home/jonas/homeassistant/get_ev_battery.py --sync-sessions
Query VM session history:
curl -s "http://127.0.0.1:8428/api/v1/query_range?query=ev_session_rate_kw&start=$(date -d '7 days ago' +%s)&end=$(date +%s)&step=3600"
HA restarted during charge window → ev_startup_recovery fires 2 min after start.
ev_min_per_pct values are initial defaults → run python3 /home/jonas/homeassistant/get_ev_battery.py --sync-sessions to fit from full history. The model is deterministic from session history so re-running is safe.
mpp_above is noisy if most sessions only go to 81% → sessions with pct_above ≥ 5% give better signal. The model converges over time.
Automation didn't run → check for HA restart at podman logs homeassistant 2>&1 | head -5.
How to edit the automations
- Edit
/home/jonas/homeassistant/automations.yaml - Reload:
Developer Tools → YAML → Reload automations - For new entities:
Developer Tools → YAML → Reload input_number/Reload timer - Always update this skill file to reflect any logic, entity, or script changes