Powervault Intergration

Hey,
I've just pushed the new release of the HA integration. Which works using the newly released local API.

Please read over the release notes to understand what's changing and ensure you have the IP address of your Powervault to hand.

It would be sensible to assign your Powervault a static IP address on your network so that it never changes.

Hi there - many thanks for your work on this, I am currently using a P4 which is working beautifully through the integration. The new HA update seems to be mostly targeted at rescuing the P3 legacy models, but what is the story with the P4, can it be used more locally too - or is it already doing that?
Many thanks

Hi Adam,

I hope you don't mind me reaching out. Shane Crosbie from Powervault suggested that I get in touch with you.

I'm completely new to Home Assistant and have very little technical knowledge, so please forgive what may be a basic question!

I have a Powervault 3 battery. Until recently, I was using Powervault's SmartSTOR service to automatically charge and discharge the battery based on the Octopus Agile tariff. Following the closure of the legacy Powervault cloud services, I've now switched the battery into local mode and have lost the SmartSTOR functionality.

Shane mentioned that it may be possible to recreate similar SmartSTOR scheduling using Home Assistant, and that you might be able to point me in the right direction.

Would you be willing to help me get this set up? I'd be very grateful if you could explain things in simple, step-by-step terms, as I'm starting from scratch with Home Assistant.

My goal is simply to have the battery automatically charge during the cheapest Agile periods and discharge during the more expensive periods, much like SmartSTOR used to do.

Many thanks in advance for any help or guidance you can provide.

Best regards, Shirin

Hi, there's no P4 local control. All the updates recently have been to keep the P3 units usable since Powervault have now disowned them totally. For the P4 and P5 units AFAIK Powervault still monitor those remotely, so limit what you can see

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 :slight_smile:
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] }}

Thanks Adam - and thanks again for the integration, it works perfectly for me.
Hope if they ever disown the P4 they do a similar handover of the API!
Cheers!