Solar-powered EV charging (surplus) automation

Introduction

I’d like to share my Home Assistant project for charging my electric vehicle using only surplus solar energy. The automation dynamically starts, stops, and adjusts the charging current based on real-time solar production and grid consumption. I’m still actively refining it and will share updates and findings in this thread. The automations were designed with the help of Claude.ai, which I used as an AI assistant to iteratively write and debug the YAML.


Goal

The goal is to automatically charge my EV only when there is sufficient surplus solar energy being fed back to the grid, minimizing grid consumption and maximizing self-consumption of solar power. The charging current is adjusted every 2 minutes based on available surplus power, scaling from 6A up to 16A depending on how much solar energy is available.


Hardware used

  • EV: Skoda Enyaq (2021)
  • EV charger: Shelly Topac (single/three-phase, controllable via Home Assistant)
  • Solar panels: 6kW peak capacity
  • Smart meter reader: DSMR P1 meter (connected to Home Assistant)
  • Home Assistant running on local hardware

Software / Integrations used

  • Home Assistant (automation platform)
  • MySkoda integration (github.com/skodaconnect/homeassistant-myskoda) — provides vehicle status, battery level, charging status, and control
  • Shelly integration (built-in HA integration) — controls the Shelly Topac EV charger
  • DSMR Reader integration — reads real-time grid feed-in and consumption from the P1 smart meter
  • Claude.ai — used as an AI assistant to help design, write, and iteratively refine the YAML automations

How it works

The project consists of three parts:

1. A template sensor that calculates the net grid feed-in:

net_feedin = solar_production - grid_consumption

This is necessary because the DSMR P1 integration in Home Assistant reports production and consumption as separate sensors. Since solar panels only feed back on one phase in my setup, the individual sensors can show simultaneous production and consumption, while the physical meter shows the correct net value. The template sensor corrects for this.

2. Start/Stop automation — checks every 6 minutes (and on trigger):

  • Starts charging when net feed-in ≥ threshold for 2 minutes
  • Stops charging when net consumption > 0.4kW for 5 minutes (and charger has been on for at least 3 minutes)
  • Supports both single-phase (with adapter plug) and three-phase charging via a dropdown helper

3. Adjust current automation — runs every 2 minutes while charging:

  • Calculates available surplus = net feed-in + current charger power (since the charger’s own consumption disappears from the feed-in sensor once it starts)
  • Maps available surplus to the optimal charge current using a fixed power table (6A–16A)
  • Only adjusts if the new value differs from the current setting and the charger has been running for at least 2 minutes

Power table used (three-phase)

Ampere Power (kW)
6A 4.0
7A 4.4
8A 5.0
9A 5.7
10A 6.4
11A 7.1
12A 7.7
13A 8.2
14A 8.9
15A 9.5
16A 10.2

Single-phase power table (with adapter plug)

Ampere Power (kW)
6A 1.38
7A 1.61
8A 1.84
9A 2.07
10A 2.30
11A 2.53
12A 2.76
13A 2.99
14A 3.22
15A 3.45
16A 3.68

Challenges

The biggest challenge has been reliably waking up the Skoda Enyaq from sleep mode via Home Assistant. The MySkoda integration communicates with the vehicle via the cloud, and when the car is asleep the sensor values (charging status, cable connected, etc.) are stale and unreliable. This causes several issues:

  • The “cable connected” binary sensor reports off even when the cable is physically plugged in
  • The “charging status” sensor shows outdated values
  • The wakeup button (button.ev_vehicle_wakeup) sends the wakeup signal but the car takes variable time to respond — sometimes 30 seconds, sometimes over a minute, sometimes not at all without opening the Skoda app
  • If the Shelly charger tries to start before the car is fully awake, the car rejects the handshake and the charger switches off within 15 seconds

Workarounds currently in use:

  1. Retry mechanism — the automation tries to turn on the Shelly charger up to 3 times with 15-second intervals
  2. Charge limit trick — temporarily changing the charge limit from 80% to 90% seems to trigger a cloud refresh and wake the car more reliably. The automation sets it to 90% at the start of the charging sequence, waits 60 seconds, then sets it back to 80% after a successful start
  3. Single-phase adapter — for days with lower solar production (e.g. winter), a physical single-phase adapter plug can be connected. The automation detects which mode is active by checking whether phase B and C show zero power, and switches to the appropriate power table and start threshold automatically. A dropdown helper allows manual override

Still working on:

  • Making the wakeup sequence more reliable without needing to open the Skoda app
  • Fine-tuning thresholds for single-phase mode
  • Testing behaviour over winter months with lower solar production

Helpers needed

Helper Type Entity ID Purpose
Solar charging active Toggle (boolean) input_boolean.ev_charging_active Master on/off switch
Charger mode Dropdown (select) input_select.ev_charger_mode 1-phase / 3-phase / Auto
Net feed-in Template sensor sensor.net_feedin Calculated net feed-in (kW)

Note: For daily charged energy I use the built-in energy sensor from the Shelly Topac charger (sensor.ev_charger_energy_today), which tracks session and daily totals natively. No utility meter helper needed.

The automations

Template sensor (add via Helper → Template sensor, or in configuration.yaml):

template:
  - sensor:
      - name: "Net feed-in"
        unique_id: net_feedin
        unit_of_measurement: kW
        state_class: measurement
        device_class: power
        state: >
          {{ states('sensor.electricity_meter_production') | float(0) -
             states('sensor.electricity_meter_consumption') | float(0) }}

Start/Stop automation:

alias: EV Solar charging - Start/Stop
description: >
  Starts charging when net feed-in is sufficient (2 min), stops when net
  consumption exceeds 0.4kW for 5 minutes. Wakes car via charge limit trick.
triggers:
  - trigger: time_pattern
    minutes: /6
  - trigger: numeric_state
    entity_id: sensor.net_feedin
    above: 1.1
    for:
      minutes: 2
  - trigger: numeric_state
    entity_id: sensor.net_feedin
    below: -0.4
    for:
      minutes: 5
variables:
  net_feedin: "{{ states('sensor.net_feedin') | float(0) }}"
  production: "{{ states('sensor.electricity_meter_production') | float(0) }}"
  consumption: "{{ states('sensor.electricity_meter_consumption') | float(0) }}"
  charger_power: "{{ states('sensor.ev_charger_power') | float(0) }}"
  charger_on: "{{ is_state('switch.ev_charger_switch', 'on') }}"
  is_single_phase: >
    {% if is_state('input_select.ev_charger_mode', '1-phase') %}
      true
    {% elif is_state('input_select.ev_charger_mode', '3-phase') %}
      false
    {% else %}
      {% if is_state('switch.ev_charger_switch', 'on') %}
        {{ states('sensor.ev_charger_phase_b_power') | float(0) < 0.1 and
           states('sensor.ev_charger_phase_c_power') | float(0) < 0.1 }}
      {% else %}
        {{ net_feedin < 3.3 }}
      {% endif %}
    {% endif %}
  start_threshold: >
    {{ 1.1 if is_single_phase == 'true' or is_single_phase == true else 3.6 }}
actions:
  - choose:
      - conditions:
          - condition: template
            value_template: "{{ charger_on }}"
          - condition: template
            value_template: "{{ net_feedin < -0.4 }}"
          - condition: template
            value_template: >
              {{ (now() -
              states.switch.ev_charger_switch.last_changed).total_seconds()
              > 180 }}
        sequence:
          - action: switch.turn_off
            target:
              entity_id: switch.ev_charger_switch
          - action: persistent_notification.create
            data:
              title: "EV charging stopped"
              message: >
                Charging stopped - net consumption was
                {{ (net_feedin * -1) | round(2) }}kW
                (production: {{ production }}kW,
                consumption: {{ consumption }}kW).
      - conditions:
          - condition: template
            value_template: "{{ not charger_on }}"
          - condition: template
            value_template: "{{ net_feedin >= start_threshold | float }}"
          - condition: state
            entity_id: input_boolean.ev_charging_active
            state: "on"
        sequence:
          - action: button.press
            target:
              entity_id: button.ev_vehicle_wakeup
          - action: number.set_value
            target:
              entity_id: number.ev_charge_limit
            data:
              value: 90
          - delay:
              seconds: 60
          - action: number.set_value
            target:
              entity_id: number.ev_charger_current_limit
            data:
              value: 6
          - action: switch.turn_on
            target:
              entity_id: switch.ev_charger_switch
          - delay:
              seconds: 15
          - if:
              - condition: state
                entity_id: switch.ev_charger_switch
                state: "off"
            then:
              - action: switch.turn_on
                target:
                  entity_id: switch.ev_charger_switch
              - delay:
                  seconds: 15
              - if:
                  - condition: state
                    entity_id: switch.ev_charger_switch
                    state: "off"
                then:
                  - action: switch.turn_on
                    target:
                      entity_id: switch.ev_charger_switch
                  - delay:
                      seconds: 15
          - if:
              - condition: state
                entity_id: switch.ev_charger_switch
                state: "on"
            then:
              - action: number.set_value
                target:
                  entity_id: number.ev_charge_limit
                data:
                  value: 80
              - action: persistent_notification.create
                data:
                  title: "EV charging started"
                  message: >
                    Charging at 6A in
                    {{ '1-phase' if is_single_phase == 'true'
                    or is_single_phase == true else '3-phase' }} mode.
                    Net feed-in: {{ net_feedin | round(2) }}kW
                    (production: {{ production }}kW,
                    consumption: {{ consumption }}kW).
            else:
              - action: number.set_value
                target:
                  entity_id: number.ev_charge_limit
                data:
                  value: 80
              - action: persistent_notification.create
                data:
                  title: "EV charging failed"
                  message: >
                    Could not start charging after 3 attempts.
                    Please check the vehicle.
    default: []
mode: single

Adjust current automation:

alias: EV Solar charging - Adjust current
description: >
  Adjusts charging current every 2 minutes based on available solar surplus.
  Only active when charger is running for at least 2 minutes.
triggers:
  - trigger: time_pattern
    minutes: /2
variables:
  net_feedin: "{{ states('sensor.net_feedin') | float(0) }}"
  charger_power: "{{ states('sensor.ev_charger_power') | float(0) }}"
  charger_on: "{{ is_state('switch.ev_charger_switch', 'on') }}"
  current_ampere: "{{ states('number.ev_charger_current_limit') | int(6) }}"
  available: "{{ net_feedin + charger_power }}"
  is_single_phase: >
    {% if is_state('input_select.ev_charger_mode', '1-phase') %}
      true
    {% elif is_state('input_select.ev_charger_mode', '3-phase') %}
      false
    {% else %}
      {{ states('sensor.ev_charger_phase_b_power') | float(0) < 0.1 and
         states('sensor.ev_charger_phase_c_power') | float(0) < 0.1 }}
    {% endif %}
  new_ampere: >
    {% if is_single_phase == 'true' or is_single_phase == true %}
      {% set table = [
          {'a': 16, 'kw': 3.68}, {'a': 15, 'kw': 3.45}, {'a': 14, 'kw': 3.22},
          {'a': 13, 'kw': 2.99}, {'a': 12, 'kw': 2.76}, {'a': 11, 'kw': 2.53},
          {'a': 10, 'kw': 2.30}, {'a': 9,  'kw': 2.07}, {'a': 8,  'kw': 1.84},
          {'a': 7,  'kw': 1.61}, {'a': 6,  'kw': 1.38}
      ] %}
    {% else %}
      {% set table = [
          {'a': 16, 'kw': 10.2}, {'a': 15, 'kw': 9.5}, {'a': 14, 'kw': 8.9},
          {'a': 13, 'kw': 8.2}, {'a': 12, 'kw': 7.7}, {'a': 11, 'kw': 7.1},
          {'a': 10, 'kw': 6.4}, {'a': 9,  'kw': 5.7}, {'a': 8,  'kw': 5.0},
          {'a': 7,  'kw': 4.4}, {'a': 6,  'kw': 4.0}
      ] %}
    {% endif %}
    {% set ns = namespace(result=6) %}
    {% for step in table %}
      {% if available >= step.kw and ns.result == 6 %}
        {% set ns.result = step.a %}
      {% endif %}
    {% endfor %}
    {{ ns.result }}
actions:
  - condition: template
    value_template: "{{ charger_on }}"
  - condition: template
    value_template: "{{ new_ampere | int != current_ampere | int }}"
  - condition: template
    value_template: >
      {{ (now() -
      states.switch.ev_charger_switch.last_changed).total_seconds()
      > 120 }}
  - action: number.set_value
    target:
      entity_id: number.ev_charger_current_limit
    data:
      value: "{{ new_ampere | int }}"
mode: single

Dashboard card:

type: vertical-stack
cards:
  - type: heading
    heading: EV Solar Charging
  - type: glance
    title: Status
    entities:
      - entity: switch.ev_charger_switch
        name: Charging
      - entity: number.ev_charger_current_limit
        name: Ampere
      - entity: sensor.ev_charger_power
        name: Power
      - entity: sensor.ev_battery_percentage
        name: Battery
  - type: entities
    title: Solar & Grid
    entities:
      - entity: sensor.electricity_meter_production
        name: Solar feed-in
      - entity: sensor.electricity_meter_consumption
        name: Grid consumption
      - entity: sensor.net_feedin
        name: Net feed-in
      - entity: input_boolean.ev_charging_active
        name: Solar charging active
      - entity: input_select.ev_charger_mode
        name: Charger mode
  - type: glance
    title: Phase power
    entities:
      - entity: sensor.ev_charger_phase_a_power
        name: Phase A
      - entity: sensor.ev_charger_phase_b_power
        name: Phase B
      - entity: sensor.ev_charger_phase_c_power
        name: Phase C
  - type: entities
    title: Vehicle
    entities:
      - entity: sensor.ev_charging_status
        name: Charging status
      - entity: sensor.ev_range
        name: Range
      - entity: number.ev_charge_limit
        name: Charge limit (%)
      - entity: device_tracker.ev_location
        name: Location
  - type: history-graph
    title: Power today
    hours_to_show: 24
    entities:
      - entity: sensor.electricity_meter_production
        name: Solar feed-in
      - entity: sensor.ev_charger_power
        name: Charger power
      - entity: sensor.ev_charger_phase_a_power
        name: Phase A
      - entity: sensor.ev_charger_phase_b_power
        name: Phase B
      - entity: sensor.ev_charger_phase_c_power
        name: Phase C
  - type: entities
    title: Energy
    entities:
      - entity: sensor.ev_charger_energy_today
        name: Energy charged today
      - entity: sensor.ev_charger_session_energy
        name: Current session energy
      - entity: sensor.ev_charger_session_duration
        name: Current session duration
  - type: entities
    title: Controls
    entities:
      - entity: switch.ev_charger_switch
        name: Manual charger on/off
      - entity: number.ev_charger_current_limit
        name: Manual ampere
      - entity: button.ev_vehicle_wakeup
        name: Wake up vehicle
      - entity: button.ev_charger_restart
        name: Restart charger

Work in progress / known issues

  • Wakeup reliability is the main pain point — still testing the charge limit trick (80% → 90% → 80%)
  • The retry mechanism (up to 3 attempts with 15-second intervals) helps but is not foolproof
  • The MySkoda integration occasionally reports stale sensor values when the car is asleep — working around this but not fully solved yet
  • Single-phase mode thresholds may need further tuning in winter
  • Will report back with findings as the season changes

I’ll keep this post updated as I refine the setup! I’ll add photo’s of the ev charger setup and dashboard asap (I don’t know when..busy familylife.. :slight_smile: