Smart Solar Dynamic Battery Night Charging

If you have a solar battery bank and require smart dynamic charging using your low cost overnight grid tariff, then here is the way to do this correctly.
Goal: Charge the battery overnight just enough to get through tomorrow, using as much free solar power as possible or available battery capacity.
This avoids excess night rate charging but ensures the predicted solar for the next day will carry you into the next charge cycle.

However, executing this perfectly in Home Assistant is notoriously tricky. Many users turn to complex tools like Predbat or EMHASS. While incredibly powerful, these tools rely on dense YAML configurations, complex mathematical models, and constant micro-adjustments that can sometimes be more frustrating than helpful with hours of problems and constant fixing – however below is the simply and easy way to do this.

If you have a large battery bank (like a 28kWh setup) paired with an Axpert/Voltronic (any) inverter managed via Solar Assistant or direct, you don’t need continuous micro-adjustments. You just need smart, reliable math and an automation that won’t rapid-cycle your hardware say every 5 minutes to ensure things are on track. This example uses Axpert and Solar Assistant but can be modified easily for any setup, send this website page to a good AI like Claude Pro or Google Pro and ask it to make the changes for you.

Here is how to build a fully localized, transparent, and self-healing overnight charging controller using just Home Assistant, Solcast (the most accurate solar predictor), and Solar Assistant.

The Core Philosophy: Why Native Automations?

The system we are building relies on three core principles:

  1. Transparency: You can see exactly why the battery is charging because you control the math.
  2. Hardware Protection (Hysteresis): We use a “Deadband” or “Freeze Zone” to prevent the inverter from rapidly clicking between charge/discharge states when the battery level fluctuates by a decimal point.
  3. Self-Healing (The Watchdog): Smart home networks drop out. This automation runs on a 5-minute watchdog timer to ensure that if an MQTT command is missed, the system auto-corrects within minutes.

Prerequisites

To implement this guide, you will need:

  • Home Assistant up and running.
  • Solar Assistant integrated into Home Assistant via MQTT.
  • Solcast installed via HACS (Free Tier is fine).
  • An Axpert/Voltronic Inverter (or similar) that allows you to change Output source priority and Charger source priority via Home Assistant entities.

Step 1: Manage Your Solcast API Limits

Solcast’s free tier limits you to 10 API calls per day. We need to ensure the absolute freshest data is pulled right before midnight. Disable auto-polling in your Solcast integration and use this automation to pull data strategically:

alias: "Solcast - Fetch API Data"
description: "Pulls Solcast data at specific times to stay within the 10 API limit."
trigger:
  - platform: time
    at: "06:00:00"
  - platform: time
    at: "09:30:00"
  - platform: time
    at: "12:00:00"
  - platform: time
    at: "16:00:00"
  - platform: time
    at: "23:55:00" # The crucial pre-midnight pull
condition: []
action:
  - action: solcast_solar.update_forecasts
    data: {}
mode: single

Step 2: Create the Target Memory (Helper)

Home Assistant needs a place to store its calculated charge target so the watchdog can reference it all night.

  1. Go to Settings > Devices & Services > Helpers.
  2. Click + Create Helper and choose Number.
  3. Name it: Nightly Charge Target
  4. Set Minimum value to your absolute worst-case reserve (e.g., 18).
  5. Set Maximum value to 100.
  6. Click Create.

Step 3: The Midnight Math

This automation triggers at 00:01. It looks at Solcast’s prediction for the upcoming day, subtracts it from your average daily house load, and adds your safety reserve. It then saves this target into the Helper we just created.
Create a new automation and click three dots to edit yaml and paste the code.

Note: Adjust daily_load to match your home’s average usage in kWh, and battery_capacity to match your bank size.


alias: "Battery - Midnight Target Calculation"
trigger:
  - platform: time
    at: "00:01:00"
action:
  - action: input_number.set_value
    target:
      entity_id: input_number.nightly_charge_target
    data:
      value: >
        {% set daily_load = 38.0 %}        # <-- your average daily kWh usage
        {% set solar_forecast = states('sensor.solcast_pv_forecast_forecast_today') | float(0) %}
        {% set battery_capacity = 28.0 %}  # <-- your battery bank size in kWh
        {% set reserve_percent = 0.20 %}   # <-- minimum reserve (18% here)
        
        {% set reserve_kwh = battery_capacity * reserve_percent %}
        {% set needed_kwh = daily_load - solar_forecast %}
        {% set target_kwh = needed_kwh + reserve_kwh %}
        
        {% set target_soc = ((target_kwh / battery_capacity) * 100) | round(0) %}
        
        {% if target_soc < 18 %}
          18
        {% elif target_soc > 100 %}
          100
        {% else %}
          {{ target_soc }}
        {% endif %}
mode: single
In the above code make sure you edit these values to match your system demands.
    data:
      value: >
        {% set daily_load = 38.0 %}        # <-- your average daily kWh usage
        {% set solar_forecast = states('sensor.solcast_pv_forecast_forecast_today') | float(0) %}
        {% set battery_capacity = 28.0 %}  # <-- your battery bank size in kWh
        {% set reserve_percent = 0.20 %}   # <-- minimum reserve (18% here)

Once you save this helper, you can click it to open the settings and edit the min/max charge rate. Ideally do 20% to 96%. Choosing 100% isn’t great for you battery but more importantly in some cases causes a flip flop situation or other issues. There is a fail safe in the yaml below to try to keep 96% as the max which you could remove if needed.

Step 4: The Dynamic Watchdog Controller

This is the brain of the operation. It manages your battery dynamically between your cheap window (e.g., 00:05 to 06:00).

Key Features:

  • Pre-Dawn Drain Protection: If your battery hits its target early (e.g., at 2:00 AM), it doesn’t just turn off the charger. It forces the house to run on the grid until 6:00 AM, freezing the battery level so it doesn’t drain before the sun comes up.
  • Dynamic EV Handling: If you plug an EV in at 1:00 AM and it aggressively drains the battery, this controller will instantly detect the drop and switch the house/EV over to the grid the moment your battery hits its safety target.
  • The 2% Freeze Zone: To prevent rapid flip-flopping, the system allows a +/- 1% variance around your target. Once it hits the target, it won’t reactivate charging or discharging unless the battery drifts significantly.

Ensure you replace the entity names with your specific Solar Assistant entities.

Create a new automation and click three dots to edit yaml and paste the code.

alias: Battery - Dynamic Overnight Controller V2
triggers:
  - at: "00:05:00"
    id: Night_Mode
    trigger: time
  - at: "06:00:00"
    id: Day_Mode
    trigger: time
  - minutes: /5
    id: Night_Mode
    trigger: time_pattern
conditions: []
actions:
  - choose:

      # ==========================================
      # SCENARIO 1: DAYTIME RESTORE (06:00 AM)
      # ==========================================
      - conditions:
          - condition: trigger
            id: Day_Mode
        sequence:
          - action: select.select_option
            target:
              entity_id: select.output_source_priority
            data:
              option: Solar/Battery/Utility
          - delay:
              seconds: 10
          - action: select.select_option
            target:
              entity_id: select.charger_source_priority
            data:
              option: Solar only

      # ==========================================
      # SCENARIO 2: NIGHT WINDOW (00:05 to 06:00)
      # ==========================================
      - conditions:
          - condition: trigger
            id: Night_Mode
          - condition: time
            after: "00:04:00"
            before: "06:00:00"
          - condition: template
            value_template: >
              {{ states('input_number.nightly_charge_target') not in ['unavailable', 'unknown', 'none']
                 and states('sensor.battery_state_of_charge') not in ['unavailable', 'unknown', 'none']
                 and states('input_number.nightly_charge_target') | float(0) > 0 }}
        sequence:
          - variables:
              effective_target: >
                {{ [states('input_number.nightly_charge_target') | float(0), 96] | min }}
              soc: >
                {{ states('sensor.battery_state_of_charge') | float(0) }}
          - choose:

              # STATE A: TOO LOW (Below target - 2%) -> Charge from grid
              - conditions:
                  - condition: template
                    value_template: "{{ soc < (effective_target - 2) }}"
                sequence:
                  - action: select.select_option
                    target:
                      entity_id: select.output_source_priority
                    data:
                      option: Solar/Utility/Battery
                  - delay:
                      seconds: 10
                  - action: select.select_option
                    target:
                      entity_id: select.charger_source_priority
                    data:
                      option: Utility first

              # STATE B: TOO HIGH (Above target) -> Drain, no grid charging
              - conditions:
                  - condition: template
                    value_template: "{{ soc > effective_target }}"
                sequence:
                  - action: select.select_option
                    target:
                      entity_id: select.output_source_priority
                    data:
                      option: Solar/Battery/Utility
                  - delay:
                      seconds: 10
                  - action: select.select_option
                    target:
                      entity_id: select.charger_source_priority
                    data:
                      option: Solar only

              # STATE C: FREEZE ZONE (Within target-2% to target) -> Hold on grid
              - conditions:
                  - condition: template
                    value_template: "{{ (effective_target - 2) <= soc <= effective_target }}"
                sequence:
                  - action: select.select_option
                    target:
                      entity_id: select.output_source_priority
                    data:
                      option: Solar/Utility/Battery
                  - delay:
                      seconds: 10
                  - action: select.select_option
                    target:
                      entity_id: select.charger_source_priority
                    data:
                      option: Solar only

mode: single

If you want rapid dynamic changes then edit the above with this change:

triggers:
  - at: "00:05:00"
    id: Night_Mode
    trigger: time
  - at: "06:00:00"
    id: Day_Mode
    trigger: time
  - minutes: /5
    id: Night_Mode
    trigger: time_pattern
  - entity_id: sensor.battery_state_of_charge
    id: Night_Mode
    trigger: state
    for:
      seconds: 30
conditions: []

Conclusion

By breaking the problem down into simple math and a state-based controller, you eliminate the black box of massive custom integrations. You know precisely why your inverter is acting the way it is, and your home battery is perfectly primed every morning to maximize your solar yield and minimize your grid bill.

Similar Posts

Leave a Reply