Hi Shirin,
I would look into EMHASS. It's a bit of a rabbit hole, but it's designed for variables tariffs. It doesn't directly control the battery, but it creates a schedule which you can then use to control the battery state.
My config is super complex because I've got my EV and AC set as additional loads for it to consider too, as well as lots of my own custom logic.
There's a megathread on here about EHMASS, and there's a post in there somewhere where I post my config from a while back (where it was somewhat simpler).
If you really want you can look at the below and try to decipher it 
This is my battery control automation, but it's based on a battery_state_forecast entity (below that) which I calculate what I want the battery to be doing
Maybe copy and paste this into ChatGPT or something and ask it to summarise and simplify for your needs
Hopefully this helps.
- id: '1687389847659'
alias: Battery State Change
description: Updates the battery status based on the sensor.battery_state_forecast
entity
triggers:
- entity_id:
- sensor.battery_state_forecast
trigger: state
- entity_id:
- input_boolean.ev_charging
trigger: state
- trigger: state
entity_id:
- sensor.powervault_charge
- trigger: state
entity_id:
- calendar.octopus_energy_a_31ba3b35_octoplus_free_electricity_session
conditions: []
actions:
- choose:
- conditions:
- condition: state
entity_id: calendar.octopus_energy_a_31ba3b35_octoplus_free_electricity_session
state: 'on'
sequence:
- device_id: 191c2b370b4fc099ab106a25a7e5919c
domain: select
entity_id: select.powervault_charge_status
type: select_option
option: force-charge
- conditions:
- condition: or
conditions:
- condition: state
entity_id: sensor.battery_state_forecast
state: Grid Import - EV
- condition: state
entity_id: sensor.battery_state_forecast
state: Grid Import
sequence:
- device_id: 191c2b370b4fc099ab106a25a7e5919c
domain: select
entity_id: select.powervault_charge_status
type: select_option
option: only-charge
- choose:
- conditions:
- condition: or
conditions:
- condition: and
conditions:
- condition: numeric_state
entity_id: sensor.powervault_charge
below: 50
- condition: time
after: '23:00:00'
- condition: numeric_state
entity_id: sensor.total_kwh_forecast_tomorrow
below: 8.5
- condition: and
conditions:
- condition: numeric_state
entity_id: sensor.powervault_charge
below: 80
- condition: numeric_state
entity_id: sensor.total_pv_estimate
below: 8.5
- condition: time
before: '20:00:00'
enabled: false
sequence:
- device_id: 191c2b370b4fc099ab106a25a7e5919c
domain: select
entity_id: select.powervault_charge_status
type: select_option
option: force-charge
enabled: false
- conditions:
- condition: or
conditions:
- condition: state
entity_id: sensor.battery_state_forecast
state: Discharge Only
sequence:
- device_id: 191c2b370b4fc099ab106a25a7e5919c
domain: select
entity_id: select.powervault_charge_status
type: select_option
option: only-discharge
- conditions:
- condition: or
conditions:
- condition: state
entity_id: sensor.battery_state_forecast
state: Normal
sequence:
- device_id: 191c2b370b4fc099ab106a25a7e5919c
domain: select
entity_id: select.powervault_charge_status
type: select_option
option: normal
- conditions:
- condition: or
conditions:
- condition: state
entity_id: sensor.battery_state_forecast
state: Grid Charge
sequence:
- device_id: 191c2b370b4fc099ab106a25a7e5919c
domain: select
entity_id: select.powervault_charge_status
type: select_option
option: force-charge
- conditions:
- condition: or
conditions:
- condition: state
entity_id: sensor.battery_state_forecast
state: Grid Export
sequence:
- device_id: 191c2b370b4fc099ab106a25a7e5919c
domain: select
entity_id: select.powervault_charge_status
type: select_option
option: force-discharge
enabled: false
mode: queued
max: 10
Battery state forecast template sensor:
- name: "Battery State Forecast History"
# This is so we can keep a history of the value, but not of the attributes, which use a lot of data in the DB
state: >-
{{ states('sensor.battery_state_forecast') }}
- name: "Battery State Forecast"
state: >-
{# Battery forecast to charge and PV is not charging #}
{%- set override = "None" %}
{%- set battery_state = "Normal" %}
{%- set battery_soc = states('sensor.powervault_charge') | float(0) %}
{%- set ev_charging = True if states('input_boolean.ev_charging') == "on" else False %}
{%- set charge_override = states('input_select.battery_charge_override') %}
{%- set override_target = charge_override | replace('%', '') | int(0) %}
{%- set current_hour = now().hour %}
{%- set current_min = now().minute %}
{% set off_peak = 0 %}
{%- if current_hour < 5 or (current_hour == 5 and current_min < 30) %}
{% set off_peak = 1 %}
{%- endif %}
{%- if current_hour == 23 and current_min >= 30 %}
{% set off_peak = 1 %}
{%- endif %}
{% if states("sensor.p_batt_forecast")|float(0) < 0
and states("sensor.p_pv_forecast")|float(0) <= 0
and off_peak
%}
{%- set battery_state = "Grid Charge" %}
{% endif %}
{% if off_peak and states("sensor.unit_load_cost")|float | int(0) <= 0.075 and states("sensor.p_grid_forecast")|float(0) > 100 and battery_state != "Grid Charge" %}
{%- set battery_state = "Grid Import" %}
{% elif off_peak and states("sensor.p_batt_forecast")|float(0) > 0 %}
{%- set battery_state = "Normal" %}
{% elif states("sensor.p_grid_forecast")|float(0) < -400 and states("sensor.p_batt_forecast")|float(0) > 400 %}
{%- set battery_state = "Grid Export" %}
{% elif states("sensor.p_grid_forecast")|float(0) < -500 and states("sensor.p_pv_forecast")|float(0) > 500 %}
{%- set battery_state = "Discharge Only" %}
{% elif battery_state != "Grid Charge" %}
{%- set battery_state = "Normal" %}
{% endif %}
{#- Solar ceiling: stop overnight grid charging when SOC is sufficient for next-day solar -#}
{#- Uses tomorrow's forecast (not the 48h soc_final forecast) to leave room for morning solar -#}
{#- Only applies when EMHASS battery control is OFF — EMHASS manages its own SOC ceiling -#}
{%- if battery_state == "Grid Charge" and states('input_boolean.emhass_battery_control') != 'on' %}
{%- set _pv_kwh = states('sensor.total_kwh_forecast_tomorrow') | float(0) * 0.8 %}
{%- set _bat_kwh = states('sensor.powervault_capacity_max') | float(18000) / 1000 %}
{%- set _deficit = [15.0 - _pv_kwh, 0] | max %}
{%- set _ceiling = [[(0.4 + (_deficit / _bat_kwh if _bat_kwh > 0 else 1)), 0.4] | max, 0.95] | min * 100 %}
{%- if battery_soc >= _ceiling %}
{%- set battery_state = "Normal" %}
{%- endif %}
{%- endif %}
{# ############ #}
{# This prevents the battery from charging the car #}
{# ############ #}
{% if ev_charging and battery_state != "Grid Charge" %}
{%- set battery_state = "Grid Import - EV" %}
{% endif %}
{% if not off_peak and battery_state == "Grid Import" %}
{%- set battery_state = "Normal" %}
{% endif %}
{#- Charge override: force charge during off-peak with 5% hysteresis -#}
{#- First entry: charge if below target. After target reached, only re-engage 5% below target. -#}
{%- if override_target > 0 and off_peak %}
{%- if battery_soc < (override_target - 5) %}
{%- set battery_state = "Grid Charge" %}
{%- elif battery_soc < override_target and states('sensor.battery_state_forecast') == "Grid Charge" %}
{%- set battery_state = "Grid Charge" %}
{%- endif %}
{%- endif %}
{{ battery_state }}
attributes:
forecast: >-
{%- set battery_forecast = state_attr('sensor.p_batt_forecast', 'battery_scheduled_power') | map(attribute='p_batt_forecast') | list %}
{%- set pv_forecast = state_attr('sensor.p_pv_forecast', 'forecasts') | map(attribute='p_pv_forecast') | list %}
{%- set grid_forecast = state_attr('sensor.p_grid_forecast', 'forecasts') | map(attribute='p_grid_forecast') | list %}
{%- set time_vals = state_attr('sensor.p_pv_forecast', 'forecasts') | map(attribute='date') | list %}
{%- set solar_forecast_total_kwh = states("sensor.emhass_pv_forecast_24hrs") | float(0) * 1000 %}
{%- set battery_percentage_forecast = state_attr('sensor.soc_batt_forecast', 'battery_scheduled_soc') | map(attribute='soc_batt_forecast') | list %}
{%- set values_all = namespace(all=[]) %}
{%- set charge_override = states('input_select.battery_charge_override') %}
{%- set override_target = charge_override | replace('%', '') | int(0) %}
{%- set battery_capacity_wh = states('sensor.powervault_capacity_max') | float(1) %}
{%- set charge_per_slot_pct = (3300 * 0.25 / battery_capacity_wh * 100) if battery_capacity_wh > 0 else 0 %}
{%- set soc_tracker = namespace(soc=states('sensor.powervault_charge') | float(0)) %}
{#- Track whether override target has been reached in forecast simulation #}
{%- set override_ns = namespace(target_reached=(soc_tracker.soc >= override_target)) %}
{#- Solar ceiling constants — mirrors Battery State Forecast current-state logic #}
{#- Ceiling is only applied when EMHASS battery control is OFF #}
{%- set _emhass_control = states('input_boolean.emhass_battery_control') == 'on' %}
{%- set _fg_pv_kwh = states('sensor.total_kwh_forecast_tomorrow') | float(0) * 0.8 %}
{%- set _fg_deficit = [15.0 - _fg_pv_kwh, 0] | max %}
{%- set _fg_ceiling = [[(0.4 + (_fg_deficit / (battery_capacity_wh / 1000) if battery_capacity_wh > 0 else 1)), 0.4] | max, 0.95] | min * 100 %}
{%- set soc_sim = namespace(soc=states('sensor.powervault_charge') | float(0)) %}
{#- EV slot awareness: build absolute epoch timestamp windows using today_at(). -#}
{#- end_ts: if end HH:MM is in the past today, add 1 day. start_ts = end_ts - duration. -#}
{%- set _slots_raw = states('sensor.ohme_home_pro_charge_slots') %}
{%- set _ev_scheduled = _slots_raw not in ['unavailable', 'unknown', 'none', ''] and _slots_raw | length > 0 %}
{%- set _ev_slots = namespace(list=[]) %}
{%- if _ev_scheduled %}
{%- for slot_str in _slots_raw.split(', ') %}
{%- set parts = slot_str.strip().split('-') %}
{%- if parts | length == 2 %}
{%- set s_mins = parts[0].split(':')[0] | int * 60 + parts[0].split(':')[1] | int %}
{%- set e_mins = parts[1].split(':')[0] | int * 60 + parts[1].split(':')[1] | int %}
{%- set dur_mins = (e_mins - s_mins) if e_mins > s_mins else (1440 - s_mins + e_mins) %}
{%- set _sns = namespace(e_ts=0) %}
{%- set _end_dt = today_at(parts[1]) %}
{%- set _sns.e_ts = as_timestamp(_end_dt + timedelta(days=1)) if _end_dt <= now() else as_timestamp(_end_dt) %}
{%- set _ev_slots.list = _ev_slots.list + [{'s': _sns.e_ts - dur_mins * 60, 'e': _sns.e_ts}] %}
{%- endif %}
{%- endfor %}
{%- endif %}
{% for i in range(battery_forecast | length) %}
{%- set battery_w = battery_forecast[i] | int(0) %}
{%- set grid_w = grid_forecast[i] | int(0) %}
{%- set pv_w = pv_forecast[i] | int(0) %}
{%- set date_ = time_vals[i][11:16] %}
{#- set off peak variable #}
{%- if date_ >= '23:30' or date_ < '05:30' -%}
{%- set off_peak = 1 -%}
{%- else -%}
{%- set off_peak = 0 -%}
{%- endif -%}
{%- set ev_charge = 0 %}
{%- set grid_charging = 0 -%}
{%- set normal = 0 %}
{%- set grid_import_only = 0 %}
{%- set grid_export_only = 0 %}
{%- set discharge_only = 0 %}
{#- if EMHASS prediction says battery should be negative watts, then show as charging #}
{%- if battery_w|float < 0
and pv_w|float <= 0 %}
{%- set v="Grid Charge" %}
{%- set grid_charging = 1 %}
{%- set grid_import_only = 0 %}
{%- set grid_export_only = 0 %}
{%- set discharge_only = 0 %}
{%- endif %}
{#- if off_peak and emhass wants a charge -#}
{%- if off_peak and battery_w|float < 0 -%}
{%- set v="Grid Charge" %}
{%- set grid_charging = 1 %}
{#- elseif its offpeak and its showing usage from the grid, then just pull from grid #}
{%- elif off_peak and grid_w|float(0) > 100 and not grid_charging %}
{%- set v="Grid Import" %}
{%- set grid_import_only = 1 %}
{%- set grid_charging = 0 %}
{#- elseif its offpeak and battery watts is positive, then show as normal #}
{%- elif off_peak and battery_w|float(0) > 0 %}
{%- set v="Normal" %}
{%- set normal = 1 %}
{#- elseif the grid is negative and the battery is positive, then show as grid export #}
{%- elif grid_w|float(0) < -400 and battery_w|float(0) > 400 %}
{%- set v="Grid Export" %}
{%- set grid_export_only = 1 %}
{#- elseif the grid is negative and the battery is positive, then show as discharge only #}
{%- elif grid_w|float(0) < -500 and pv_w|float(0) > 500 %}
{%- set v="Discharge Only" %}
{%- set discharge_only = 1 %}
{#- otherwise battery is normal state #}
{%- elif not grid_charging and not grid_import_only %}
{%- set v="Normal" %}
{%- set normal = 1 %}
{% endif %}
{#- Solar ceiling: downgrade Grid Charge to Normal if simulated SOC >= ceiling -#}
{%- if v == "Grid Charge" and not _emhass_control and soc_sim.soc >= _fg_ceiling %}
{%- set v = "Normal" %}
{%- set grid_charging = 0 %}
{%- set normal = 1 %}
{%- endif %}
{#- Mark target reached once SOC hits or exceeds override_target #}
{%- if soc_tracker.soc >= override_target %}
{%- set override_ns.target_reached = True %}
{%- elif soc_tracker.soc < (override_target - 5) %}
{#- SOC has dropped 5% below target: re-enable charging #}
{%- set override_ns.target_reached = False %}
{%- endif %}
{#- Charge override: force charge during off-peak with 5% hysteresis -#}
{#- Charge if below (target-5), or if still in initial charge cycle (target not yet reached) -#}
{%- if override_target > 0 and off_peak and not override_ns.target_reached and soc_tracker.soc < override_target %}
{%- set v = "Grid Charge" %}
{%- set grid_charging = 1 %}
{%- set normal = 0 %}
{%- set grid_import_only = 0 %}
{%- set grid_export_only = 0 %}
{%- set discharge_only = 0 %}
{%- set soc_tracker.soc = [soc_tracker.soc + charge_per_slot_pct, 100] | min %}
{%- endif %}
{#- EV slot: mark timestep as Grid Import - EV using absolute epoch timestamp comparison -#}
{%- if _ev_scheduled and v != "Grid Charge" %}
{%- set _forecast_ts = as_timestamp(time_vals[i]) %}
{%- set _in_slot = namespace(val=false) %}
{%- for slot in _ev_slots.list %}
{%- if _forecast_ts >= slot.s and _forecast_ts < slot.e %}
{%- set _in_slot.val = true %}
{%- endif %}
{%- endfor %}
{%- if _in_slot.val %}
{%- set v = "Grid Import - EV" %}
{%- set ev_charge = 1 %}
{%- set grid_import_only = 1 %}
{%- set grid_charging = 0 %}
{%- set normal = 0 %}
{%- set grid_export_only = 0 %}
{%- set discharge_only = 0 %}
{%- endif %}
{%- endif %}
{#- Advance simulated SOC for next slot: negative battery_w = charging -#}
{#- Use actual charge rate (3196W) not EMHASS trickle value — Powervault ignores power setpoints #}
{%- set _sim_batt_w = -3196 if battery_w | float < 0 else battery_w | float %}
{%- set _soc_delta = (-_sim_batt_w) * 0.25 / battery_capacity_wh * 100 if battery_capacity_wh > 0 else 0 %}
{%- set soc_sim.soc = [[soc_sim.soc + _soc_delta, 10] | max, 100] | min %}
{# {{ date_ }}, {{ off_peak }}, Battery {{ battery_w }}, Grid {{ grid_w }}, {{ v }} #}
{%- set v={"date": time_vals[i], "battery_state": v, "grid_charging": grid_charging, "normal": normal, "grid_import_only": grid_import_only, "grid_export_only": grid_export_only, "discharge_only": discharge_only, "ev_charge": ev_charge} %}
{%- set values_all.all = values_all.all + [ v ] %}
{%- endfor %} {{ (values_all.all)[:96] }}