batpred icon indicating copy to clipboard operation
batpred copied to clipboard

Freeze charge - LuxPower inverters.

Open brickatius opened this issue 1 month ago • 15 comments

Predbat version - 8.27.19 (and previous) apps.yaml (redacted) - apps-4.yaml.txt Debug yaml predbat_debug.yaml.txt

The Issue I'm trying to get a LuxPower inverter to behave as closely as possible to what a GE inverter does when 'FrzChrg' (charge_freeze_service) is called by the plan. The description from the docs is this:

Freeze charging - The battery is charging but the current battery level (SoC) is frozen (held). Think of it as a charge to the current battery level. The grid or solar covers any house load. If there is a shortfall of Solar power to meet house load, the excess house load is met from grid import, but if there is excess Solar power above the house load, the excess solar will be used to charge the battery,

This reads just like Demand mode but with any house load, which might have come from the battery, coming from the grid instead.

Proposed implementation LuxPower inverters have a Charge Priority / Charge First option which when enabled stops the battery from discharging. All of the house load comes from the grid. Any solar power is used to charge the battery. Whilst this is identical to what is described above when there is no solar, it needs to be manipulated when there is solar power. To this end I have a template sensor helper (Solar/Home difference) to calculate the difference in wattage between the PV output and the house load thus:

{{ states("sensor.lux_solar_output_live") | float / 1 - states("sensor.lux_home_consumption_live") | float / 1 }}

A positive value would indicate that no action needs to be taken and Charge Priority is not required because a) The battery is not discharging b) There is sufficient solar power to meet the house load, therefore grid import is not needed. c) Excess solar is used to charge the battery A negative value means that the battery would be discharging to meet the house load which is not what we want. To keep the battery SoC frozen, Charge Priority needs to be switched on. Caveat - when Charge Priority is enabled, none of the solar power is used to meet the house load but goes to charging the battery instead. There is a trade-off between more power being pulled from the grid than the FrzChrg description above but on the other hand more power has gone into the battery. I don't think this is very significant because the next time the Plan is calculated the battery SoC will be slightly higher than the plan was expecting and will be adjusted accordingly.

I have an automation, triggered by the Solar/Home helper, to switch Charge Priority ON/OFF as required.

alias: Lux Predbat Freeze Charge
description: >-
  Predbat charge_freeze_service switching. Turn on Charge Priority if Home
  Comsumption is greater than Solar Output.
triggers:
  - trigger: state
    entity_id:
      - sensor.solar_home_difference
conditions: []
actions:
  - choose:
      - conditions:
          - condition: numeric_state
            entity_id: sensor.solar_home_difference
            below: 0
        sequence:
          - action: switch.turn_on
            metadata: {}
            data: {}
            target:
              entity_id: switch.lux_charge_priority
      - conditions:
          - condition: numeric_state
            entity_id: sensor.solar_home_difference
            above: 0
        sequence:
          - action: switch.turn_off
            metadata: {}
            data: {}
            target:
              entity_id: switch.lux_charge_priority
mode: single

This automation is enabled when the charge_freeze_service is called like this:

charge_freeze_service:
    - service: automation.turn_on
      entity_id: "automation.lux_predbat_freeze_charge"

You can see from the log and the automation timeline that everything is working as expected. The automation is running and switches the Charge Priority ON of OFF triggered by the state of the helper. Positive value = OFF. Negative value = ON.

2025-11-10 12:21:30.431662: Inverter 0 Calling service charge_freeze_service
domain charge service_name automation/turn_on with data {'entity_id': 'automation.lux_predbat_freeze_charge'}
Image

When Charge Priority is OFF as in Option 2 in the image above you would expect to see something like this

Image

but we're getting this instead

Image

The problem It would appear that Predbat is ignoring the state of the Charge Priority switch in the automation altogether and just 'doing its own thing' when FrzChrg is called. I would really like to get this working and would appreciate any help or advice.

Apologies for this being long-winded but I wanted to give as much info as possible. Some of the dates and times I've used in the examples don't match up but the problem is persistent.

brickatius avatar Nov 14 '25 15:11 brickatius

I followed your logic fine up to

When Charge Priority is OFF as in Option 2 in the image above you would expect to see something like this

then

I didn't really understand the diagrams first of all, and the 'predbat is ignoring the state' doesn't sound correct either

You have an automation that sets charge priority. That automation is enabled when predbat goes into charge freeze mode, but what disables the automation? You would need a corresponding command to disable the automation when Charge Stop is called.

I don't really like the idea of enabling and disabling the automation when you enter/exit Charge Freeze mode, that seems fragile to me and all to easy for the automation to be mis-set. I'd suggest it better that the automation be enabled all the time but you add a condition into the automation to check whether predbat is in freeze charge status or not.

I'd also suggest that since the watts difference value isn't important you would be better having a binary sensor to indicate if solar>house or not, and use that in the automation. The current automation will fire every single time the solar-house figure changes, even by a watt, which means it will be firing commands to your inverter all the time. You could leave it as it is if you want to have the watt figure on a dashboard, but put extra logic in to not change the inverter mode if it's already what it needs to be.

Finally I'd suggest that you add a recorder exclude in configuration.yaml for the solar-house sensor (whether watts or binary sensor) as there's no real value in keeping the history of this sensor and it will rapidly fill your HA database with junk.

It would appear that Predbat is ignoring the state of the Charge Priority switch in the automation altogether and just 'doing its own thing' when FrzChrg is called.

I don't understand this point either. Predbat doesn't look at the charge priority switch, it simply enables the automation when it enters charge freeze mode.

gcoan avatar Nov 14 '25 20:11 gcoan

Thanks for your response Geoffrey @gcoan - greatly appreciated. I'll try to deal with it bit by bit.

  1. Diagrams The HA LuxPower integration works by connecting locally to the dongle/data logger over WiFi. This avoids having to rely on a connection to the Lux servers. As a result you can poll the inverter every 20 seconds or so (more in point 4 below). Of the 206 entities exposed by the HA integration there are several 'live' ones which are used in the flow diagrams. These are
  pv:
    combined_entities:
      - sensor.lux_solar_output_live   
 grid:
   flow_entities:
     - sensor.lux_grid_flow_live   
consumption:
    home_entities:
       - sensor.lux_home_consumption_live         
 battery:
   soc_entities:
      - sensor.lux_battery_soc_corrected
                 

As a result you can get a more or less live representation of the current flowing from the solar panels to the house and also to and from the battery and the grid. This is what is shown in the diagrams. (I use the PV and consumption entities in the Solar/home helper) Think of the diagrams as a clock face with the inverter at the centre. 12 o'clock is the solar panels, 3 is the grid, 6 is the house and 9 o'clock is the battery. The XXX W figures show the power in watts and the chevrons indicate the direction of flow. The first diagram above shows what normally happens when the inverter is in Demand mode. Solar powers the house and any excess feeds into the battery. If there were to be a shortfall the extra needed by the house would come from the battery with the chevrons pointing from the battery towards the inverter. When there is no flow there are no chevrons. The second diagram shows what happens when Predbat is in Charge Freeze mode or if Predbat is in Read Only mode what happens when the Lux Charge Priority switch is ON - House load comes from the grid. Notice the 145 W coming straight from the grid into the house.

  1. Sorry, I didn't give enough information here (it's in the uploaded apps.yaml). The automation is disabled by the charge_stop_service. I've also set it to turn off the Charge Priority switch in case it was in the ON state when the FrzChrg slot ended. You'll see that the charge_stop_service also is used to turn off the force charge. All this works fine.
charge_stop_service:
    - service: switch.turn_off
      entity_id: "switch.lux_ac_charge_enable"
    - service: switch.turn_off
      entity_id: "switch.lux_charge_priority"
    - service: automation.turn_off
      entity_id: "automation.lux_predbat_freeze_charge"   
  1. I'm not sure I agree about the 'fragility' of enabling and disabling the automation with the charge_freeze_service and charge_stop_service. I'd have thought it was pretty robust. Surely it's no different from enabling and disabling the force charge and discharge services for example. I wouldn't have thought it a good idea to have the automation running all the time and turning the Charge Priority switch on and off at random times throughout the day. Really interestingly, even though Predbat is supposed to be in control of the inverter, if you turn the Charge Priority switch on when Predbat is in Demand mode the inverter responds to this switch and goes into Charge Priority mode.

  2. I thought about your binary sensor but I don't think it would work because the automation would only be triggered by a change, not the current state. I'll certainly look into the actual logic of the present automation. As it stands, it doesn't fire every time the Solar/House value changes as the data from the dongle/data logger is only refreshed every 20 seconds (there's a separate Blueprint for that see below) - that interval can be changed but is the recommended minimum time. UPDATE: I've edited the automation to add an extra condition to check on the state of the switch.
    Blueprint for refresh interval

blueprint:
  name: LUX Refresh Interval
  description: Change the interval for the LUX inverter to refresh
  domain: automation
  input:
    luxpower_device:
      name: Luxpower Inverter
      description: Select the inverter to refresh data from.
      selector:
        device:
          filter:
            - integration: luxpower
          multiple: false
    luxpower_refresh_interval:
      name: Refresh Interval
      description: Input the interval in seconds to refresh data.
      selector:
        number:
          max: 300
          min: 20
          step: 20
      default: 120

variables:
  luxpower_device_var: !input luxpower_device
  refresh_interval_seconds: !input luxpower_refresh_interval
  # Find the data_received_time entity from the luxpower device:
  lux_data_received_time_entity: >
    {{ device_entities(luxpower_device_var) | select("search", "data_received_time") | list | first }}

trigger:
  # Template triggers only update every minute, so use time pattern to check every 20 seconds instead.
  # It will only run if the condition below is true regardless of this time pattern value.
  - trigger: time_pattern
    seconds: /20

conditions:
  # Only update if the last update time is more than refresh interval ago (-1 second)
  # The -1 second is to allow for slight deviation between the trigger and the last data received time.
  - condition: template
    value_template: >
      {{now().timestamp() - state_attr(lux_data_received_time_entity, "timestamp") > refresh_interval_seconds - 1 }}

action:
  - action: luxpower.luxpower_refresh_registers
    data:
      dongle: >-
        {{ device_attr(luxpower_device_var, "name") }}
      bank_count: 2

mode: single

LUX refresh automation

alias: LUX Refresh Interval
description: ""
use_blueprint:
  path: homeassistant/refresh_interval.yaml
  input:
    luxpower_device: 53b47dc71e7cd328925306f349793867
    luxpower_refresh_interval: 20

Modified charge freeze automation

alias: Lux Predbat Freeze Charge
description: >-
  Predbat charge_freeze_service switching. Turn on Charge Priority if Home
  Comsumption is greater than Solar Output.
triggers:
  - trigger: state
    entity_id:
      - sensor.solar_home_difference
conditions: []
actions:
  - choose:
      - conditions:
          - condition: numeric_state
            entity_id: sensor.solar_home_difference
            below: 0
          - condition: template
            value_template: "{{ is_state('switch.lux_charge_priority', 'off') }}"
        sequence:
          - action: switch.turn_on
            metadata: {}
            data: {}
            target:
              entity_id: switch.lux_charge_priority
      - conditions:
          - condition: numeric_state
            entity_id: sensor.solar_home_difference
            above: 0
          - condition: template
            value_template: "{{ is_state('switch.lux_charge_priority', 'on') }}"
        sequence:
          - action: switch.turn_off
            metadata: {}
            data: {}
            target:
              entity_id: switch.lux_charge_priority
mode: single
  1. Yes a recorder exclude for the helper makes sense although the other 'live' entities must also produce a fair amount of data.
recorder:
  exclude:
    entities:
      - sensor.solar_home_difference 
  1. This really is the crux of the matter and what I can't understand. I ran an experiment today. I've got a smart plug into which I plugged a lamp with the lamp switch to ON. I added a switch for this into the charge_freeze automation like this.
alias: Lux Predbat Freeze Charge
description: >-
  Predbat charge_freeze_service switching. Turn on Charge Priority if Home
  Comsumption is greater than Solar Output.
triggers:
  - trigger: state
    entity_id:
      - sensor.solar_home_difference
conditions: []
actions:
  - choose:
      - conditions:
          - condition: numeric_state
            entity_id: sensor.solar_home_difference
            below: 0
        sequence:
          - action: switch.turn_on
            metadata: {}
            data: {}
            target:
              entity_id: switch.lux_charge_priority
          - action: switch.turn_on
            metadata: {}
            data: {}
            target:
              entity_id: switch.smart_plug
      - conditions:
          - condition: numeric_state
            entity_id: sensor.solar_home_difference
            above: 0
        sequence:
          - action: switch.turn_off
            metadata: {}
            data: {}
            target:
              entity_id: switch.lux_charge_priority
          - action: switch.turn_off
            metadata: {}
            data: {}
            target:
              entity_id: switch.smart_plug
mode: single

Now here's the interesting thing - the lamp turned on and off exactly in accordance with the automation conditions, that is a) house load > solar output - Lamp ON. b) house load < solar output - Lamp OFF The Charge Priority switch does exactly the same a) house load > than solar output - Charge Priority ON b house load < solar output - Charge Priority OFF but the crucial thing is that the inverter does not respond accordingly and behaves as if the Charge Priority switch is ON all of the time, as in the image below. Even though, when I took this screenshot, the lamp was OFF and the Charge Priority switch was also OFF it shows that the house load is coming from the grid, just as if the switch was ON.

Image

In your last paragraph you say this ' Predbat doesn't look at the charge priority switch, it simply enables the automation when it enters charge freeze mode. If that is the case and Predbat isn't looking at the charge priority switch, why is it doing anything at all? Until Predbat enters charge freeze mode there has been no mention anywhere of anything to do with Charge Priority. I just don't get how the inverter decides to behave how it does when the charge freeze service is called.

P.S. For completeness this is the relevant part of the settings page of the LuxPower web portal. The enable/disable buttons are the lux integration's 'switch.lux_charge_priority' on/off. The enable/disable state also changes in line with the automation conditions but as above the state of the switch is ignored. I've set the Charge First start and end times to 00:00 and 23:59 respectively just in case, because Predbat has no knowledge of these times.

Image

Please don't anyone think that I'm complaining about Predbat - I 'm not! I think it's brilliant. I'd just like to be able to make it work as intended for every inverter.

brickatius avatar Nov 16 '25 13:11 brickatius

Fortunately the sun is out so there's more than enough solar to power the house. Just out of curiosity I decided to trick Predbat into thinking that the charge_start_service (Chrg) was the freeze_charge_service (FrzChrg) by editing the apps.yaml like this:

charge_start_service:
    #service: switch.turn_on
    #entity_id: "switch.lux_ac_charge_enable"
    service: automation.turn_on
    entity_id: "automation.lux_predbat_freeze_charge"

#scheduled_charge_enable:
   # - switch.lux_ac_charge_enable

I then ran a Manual Charge and lo and behold my inverter behaved exactly as I hoped it would when the charge_freeze_service (FrzChrg) is called. The automation turned on and ran according to the conditions.

  1. When the solar power was more than the house load the inverter was running in Demand mode i.e house load was met by the solar power and the excess solar went to charging the battery. Nothing coming from the grid. This conforms with the freeze charging description.
  2. I then turned on the double oven so that the house load was then greater than the solar power. The aforementioned Charge Priority switch turned on and the inverter went into Charge Priority / Predbat charge freeze mode (subject of course to the caveat mentioned at the top of this issue.) i.e. all of the house load then came from the grid as expected and the solar power went into the battery. The most important part of the Charge Freeze criteria (battery SoC held) was met!
  3. If there had been no solar power all of the house load would have been coming from the grid. This too is just as described in the documentation. Battery SoC held.

All this leads me to believe that there is something in the Predbat code, when the charge_freeze_service is called, that is overriding the settings in the automation and preventing them from being executed.

I notice that Trefor @springfall2008 has been doing a lot of work with Fox inverters recently, which is no doubt greatly appreciated. Whilst I appreciate that this is no deal breaker, Is this not a case for a bit of tweaking of Predbat to better accommodate LuxPower inverters?

brickatius avatar Nov 17 '25 10:11 brickatius

I'll try and have another look at this, not had a chance to read your detailed response yet @brickatius

Was helping out Huawei users last night with their config. And got Sigenstore questions and one Powerwall in the queue.

Too busy!

The issue is probably the way Predbat expects freeze charge to work on givenergy inverters that is copying over into behaviour on your inverter

gcoan avatar Nov 17 '25 11:11 gcoan

If you think it is sortable and are satisfied that it's a good enough compromise, I'd be more than happy to help draft some documentation. We could include the discharge_freeze_service too. That one's dead easy and already works fine.

brickatius avatar Nov 17 '25 13:11 brickatius

I'm sure it's sortable, just need to spend enough time looking at it properly, which I haven't had!

Let's do freeze discharge first as that's working and I can slot that straight into the documentation. I have a number of other changes pending for other inverters so can include it in that

gcoan avatar Nov 17 '25 14:11 gcoan

Hi Geoffrey @gcoan, Not all LuxPower inverters have the necessary feature to use discharge freeze service so maybe something along these lines. I hope it's reasonably foolproof!

Discharge Freeze Service.

If you have a LuxPower Hybrid Inverter you should enable the Predbat discharge freeze service. Enabling this utilises the Charge Last feature available on these inverters and will ensure you get the most out of Predbat.

Use the built-in editor in Predbat to edit the apps.yaml file.

Firstly look for this line in the Inverter: section

support_discharge_freeze: False

and change False to True so that it looks like this

support_discharge_freeze: True

Next, look just below this, in the charge/discharge services start and stop section. Find the discharge_stop_service: and edit it by adding 2 more lines so that it looks like this

discharge_stop_service:
  - service: switch.turn_off
    entity_id: "switch.lux_force_discharge_enable"
  - service: switch.turn_off
    entity_id: "switch.lux_charge_last"

Then add the following lines below the lines you have just added

discharge_freeze_service:
  - service: switch.turn_on
     entity_id: "switch.lux_charge_last"

The whole section should look like this

charge_start_service:
  - service: switch.turn_on
     entity_id: "switch.lux_ac_charge_enable"
charge_stop_service:
  - service: switch.turn_off
    entity_id: "switch.lux_ac_charge_enable"
discharge_start_service:
  - service: switch.turn_on
     entity_id: "switch.lux_force_discharge_enable"
discharge_stop_service:
  - service: switch.turn_off
     entity_id: "switch.lux_force_discharge_enable"
  - service: switch.turn_off
     entity_id: "switch.lux_charge_last"
discharge_freeze_service:
  - service: switch.turn_on
     entity_id: "switch.lux_charge_last"

Click the Save button. This will save your changes and restart Predbat.

Finally, go to the Config tab in Predbat and scroll down to find the Set Export Freeze Only switch and turn it ON. Whilst you are on the page you should check that the Set Export Freeze and Set Charge Freeze switches are OFF.

After the Predbat Plan has recalculated you may notice some 'FrzExp' in the state column next to some slots.

Hope you find this useful. Richard

PS Not sure why, but some of the indentations have gone awry.

brickatius avatar Nov 17 '25 17:11 brickatius

Hi Richard @brickatius I have updated the documentation for LuxPower freeze discharge, have a look at the edits and let me know what you think LuxPower discharge freeze

I've simplified what you wrote a bit, preferring not to have step by step instructions as this isn't the general way we have written the documentation, but instead guide people as to what needs to be set. I've also added the discharge freeze entries into the template apps.yaml, but commented out, to make it easier for people to change the configuration.

I was a bit surprised/confused by your last bit:

Finally, go to the Config tab in Predbat and scroll down to find the Set Export Freeze Only switch and turn it ON. Whilst you are on the page you should check that the Set Export Freeze and Set Charge Freeze switches are OFF.

https://springfall2008.github.io/batpred/customisation/#inverter-control-options

Export freeze only is an expert mode setting (so need people to turn expert mode on first), and enables freeze export but prevents forced export, so unless you are on a brown tariff (e.g. Scottish Power) then this isn't normally needed?

Also if you turn set_export_freeze off then as I read the documentation ("but export freeze can be used (if enabled)") then turning this off will prevent export freeze from operating.

We also have issue #2197 which is I think the same topics of freeze charge/discharge for LuxPower. There is more on that one, and more contributors, but your more recent analysis above is only on this issue. Do we need both issues?

gcoan avatar Nov 17 '25 19:11 gcoan

Happy to go along with all of this. I was wondering if you might make the changes to the template instead. Good call. Sorry if I was a bit confusing/confused about the Config settings. I wasn’t aware the Set Export Freeze Only was an expert only thing and what it implied. Clearly Set Export Freeze needs to be ON. I think we have just about exhausted issue #2197. I was wondering whether I should have tacked all of this onto the end of #2197 but decided it was going off at a bit of a tangent. Is it worth closing it with possibly a pointer towards this issue?

brickatius avatar Nov 17 '25 20:11 brickatius

Updated the documentation, I'd used Hybrid because that's what you'd referred to ! Now says Charge Last feature https://github.com/gcoan/batpred/commit/e553a2ee80119d83846d54dee60e2809eff3b7b6

Will close 2197 as a duplicate of this so it'll link to this.

Then we can look at charge freeze

gcoan avatar Nov 17 '25 22:11 gcoan

The "Lux Predbat Freeze Charge" automation proposed above looks good @brickatius. The only think I can think is it's triggered by a change of sensor.solar_home_difference

It needs an additional condition I think, it should do nothing, unless batpred.status = freeze_charging If batpred is no longer freeze_charging it will have executed the discharge_stop_service so this automation should no longer run. Until such time as the start freeze charge service runs it again, and the status is freeze_charging and it begins reacting again to the change in sensor.solar_home_difference

Bearing in mind, load isn't really live, lux_home_consumption_live it's a snapshot at a moment in time, I've got my 10kW inverter refreshing ~every 20s

raldred avatar Nov 17 '25 23:11 raldred

@raldred Thanks for the positive feedback. Im not sure that I understand what you are getting at regarding the helper. The automation will only be running when freeze charge is active and even then will only be doing a minimal amount of switching so scarcely any writes to the inverter. Are you suggesting that we try to disable the helper when Predbat is in any mode other than freeze charge? I don’t think that is possible and besides, it’s only going to be doing a tiny calculation every 20 seconds. The memory and CPU usage will be minuscule. If you set the recorder to exclude it from the history as well, its impact on HA will be virtually nonexistent.

brickatius avatar Nov 18 '25 09:11 brickatius

Sorry I missed that you'd added a call to automation.turn_off to the stop service. :D So the automation can be run by any triggers you like.

Since you already have it watching

triggers:
  - trigger: state
    entity_id:
      - sensor.solar_home_difference

It doesn't even need to be triggered by the start/stop service. It could just watch for batpred.status and bail out early if it's not the status we want for this. However I think where you have it will work fine.

raldred avatar Nov 18 '25 09:11 raldred

By adding a condition like this...

conditions:
  - condition: state
    entity_id: predbat.status
    state: freeze_charging

raldred avatar Nov 18 '25 09:11 raldred

Yes of course we could do what you suggest @raldred however if we do that, it is not consistent with calling the other services in the apps.yaml and the control of the automation, which would otherwise be running all the time, would lie outside of Predbat. As you said, it will work fine anyway.

brickatius avatar Nov 18 '25 10:11 brickatius

Thinking about it, there's absolutely no reason not include the predbat.status condition in the automation anyway. Once the automation is running it will make it much more robust.

Revised automation - tested and working

alias: Lux Predbat Freeze Charge
description: >-
  Predbat charge_freeze_service switching. Turn on Charge Priority if Home
  Comsumption is greater than Solar Output.
triggers:
  - trigger: state
    entity_id:
      - sensor.solar_home_difference
conditions:
  - condition: state
    entity_id: predbat.status
    state:
      - Freeze charging
actions:
  - choose:
      - conditions:
          - condition: numeric_state
            entity_id: sensor.solar_home_difference
            below: 0
          - condition: template
            value_template: "{{ is_state('switch.lux_charge_priority', 'off') }}"
        sequence:
          - action: switch.turn_on
            metadata: {}
            data: {}
            target:
              entity_id: switch.lux_charge_priority
      - conditions:
          - condition: numeric_state
            entity_id: sensor.solar_home_difference
            above: 0
          - condition: template
            value_template: "{{ is_state('switch.lux_charge_priority', 'on') }}"
        sequence:
          - action: switch.turn_off
            metadata: {}
            data: {}
            target:
              entity_id: switch.lux_charge_priority
mode: single

brickatius avatar Nov 18 '25 12:11 brickatius

@brickatius I've been working your way through the original issue text, and thanks for your explanation of the screenshots, they're power flows, I have similar for my own inverters which are polled every 30 seconds, so very much the same display: Image

One thing that caught my eye as being a potential problem area is your template solar/home difference sensor. You have:

{{ states("sensor.lux_solar_output_live") | float / 1 - states("sensor.lux_home_consumption_live") | float / 1 }}

I would be concerned that the order of precedence of the operators might not give you the desired result. I have seen problems before that Jinja doesn't execute how you expect it to do.

To resolve this, put extra brackets in to force the execution order, e.g.

{{ ( (states("sensor.lux_solar_output_live") | float) / 1) - ( (states("sensor.lux_home_consumption_live") | float ) / 1) }}

Is there a reason for the /1 in the template?

But you say the automation is being triggered based on the state of the helper so maybe this isn't a problem. But I'd add brackets to be doubly sure.

On the binary sensor vs watts value, I think we're at slightly crossed purposes.

At the moment you have a template that is triggered when either of the two source watt values that it is listening on (solar pv power and house load power). Its likely that those values will change every 20 seconds so the template will be updated every 20 seconds.

Every 20 seconds the template will have a new value, this will be written to the HA database as a state change, and this in turn will cause the automation to execute as its triggered on change of numeric state for solar_home_difference.

As long as predbat is in Freeze charging status then the automation will either turn on or turn off the lux_charge_priority switch, regardless of what state it is currently in.

Hopefully with this longer explanation you see my concerns, but whilst this may work, it isn't very efficient.

You can't get away from having to have the template triggered every 20 seconds on a change of home or solar power because you want to work out the difference between them, but what happens then you can make much smarter.

I was suggesting that rather than the solar_home_difference being a watt value, you don't need that. All you care about is whether the difference is positive or negative so instead use a binary_sensor for the difference

e.g.

{{ (states("sensor.lux_solar_output_live") | float) >= (states("sensor.lux_home_consumption_live") | float) }}

Or whichever condition you want for the 'sense' of the binary_sensor.

By having a binary sensor state drive the automation you now only trigger the automation when solar becomes greater than house load and vice versa.

Next point is to use a recorder exclude in apps.yaml to no longer store the value of the solar_home_difference in the HA states database. The value will still trigger the automation but it will reduce database clutter. Obviously only do this when you have thoroughly tested everything. Swapping to a binary sensor will reduce database clutter a lot anyway so this is less important than if you use a watt value. Yes the live entities will add a lot to the database. You can either exclude them or you can purge the entities database regularly. I have an automation that does things like this:

action: recorder.purge_entities
alias: Purge Power grid to battery, ASHP and PV string power sensor data to 5 days
data:
  keep_days: 5
  entity_id:
    - sensor.g_xxx_grid_to_battery
    - sensor.ashp_power

Another optimisation I was going to suggest is to not turn on or off the switch in the automation if its already in the state you want it to be. Again if you move to a binary sensor this is less important because the automation won't be triggered all the time. I see you have done this already in the latest automation. It's a style thing, I personally would just check the state rather than have a template to check if the switch is on or off, e.g.

          - condition: state
            entity_id: switch.lux_charge_priority
            state:
              - "off"

Point 6, the why doesn't it work ...

Now here's the interesting thing - the lamp turned on and off exactly in accordance with the automation conditions, that is a) house load > solar output - Lamp ON. b) house load < solar output - Lamp OFF The Charge Priority switch does exactly the same a) house load > than solar output - Charge Priority ON b house load < solar output - Charge Priority OFF but the crucial thing is that the inverter does not respond accordingly and behaves as if the Charge Priority switch is ON all of the time, as in the image below. Even though, when I took this screenshot, the lamp was OFF and the Charge Priority switch was also OFF it shows that the house load is coming from the grid, just as if the switch was ON.

Your test with adding the lamp to the automation proves that everything hangs together, I assume you have checked the live sensor values in HA, your house/solar difference value, the traces for the automation, etc and everything looks as it should do. I think you've said as much, but double checking.

if that is the case and Predbat isn't looking at the charge priority switch, why is it doing anything at all? Until Predbat enters charge freeze mode there has been no mention anywhere of anything to do with Charge Priority. I just don't get how the inverter decides to behave how it does when the charge freeze service is called.

Luxpower is a custom inverter type for predbat so predbat has no inbuilt knowledge of your inverter or your inverter controls. Everything is configured via apps,yaml so I'm assuming that the charge priority switch isn't referred to anywhere else in your apps.yaml?

Two possible hypothesis before I read your next trial of swapping the charge and charge freeze services.

  1. You've checked the charge priority switch in HA and it is set to the correct values when you expect it to be, and its not changing value at any other times? (check state history)
  2. the power flow diagrams you are using to work out what the inverter is doing, where are they from? Are they from the luxpower server or are they from the live data off your inverter? I was wondering if they are off the server then could it be that the server data is out of date? I use the HACS Power flow plus card https://github.com/flixlix/power-flow-card-plus to show my own inverter power flow info, directly off the live (every 30 seconds) inverter power sensors.

Your test of charge freeze automation running when charge starts is interesting. Again confirming that your solution hangs together. It doesn't help explain why freeze charge doesn't.

All this leads me to believe that there is something in the Predbat code, when the charge_freeze_service is called, that is overriding the settings in the automation and preventing them from being executed.

As I said before, predbat has no knowledge of the Luxpower inverter other than what is configured in apps.yaml so it can't be manipulating the inverter directly. But I suspect what is happening is the sequence of things predbat does when it starts charging is not quite the same as when it starts freeze discharging and that is what is causing the inverter to not behave the way you expect it to in freeze charge mode.

I can check the code, but I think you can probably check the behaviour by looking at your inverter sensor settings at the time the charge and freeze charge started. My suspicion is to look at the discharge rate,

Here's the logs from my inverter when a charge starts:

2025-11-18 12:30:16,007 - G - write       -  [INFO    ] - Setting Charge Slot 1 to: 13:00 - 16:00 was a success
2025-11-18 12:30:16,502 - G - write       -  [INFO    ] - Setting Charge Schedule to enable was a success
2025-11-18 12:30:17,014 - G - write       -  [INFO    ] - Setting Charge Target 80 was a success
2025-11-18 13:00:26,200 - G - write       -  [INFO    ] - Setting Battery Pause Mode to PauseDischarge was a success

and when a freeze charge starts:

2025-11-18 14:20:25,509 - G - write       -  [INFO    ] - Setting battery charge rate 1300 was a success
2025-11-18 14:20:36,310 - G - write       -  [INFO    ] - Setting Charge Slot 1 to: 14:00 - 16:00 was a success
2025-11-18 14:20:36,791 - G - write       -  [INFO    ] - Setting Charge Schedule to enable was a success

Crucial difference from the logs is that a charge sets the battery pause mode whereas a freeze charge doesn't. You don't have battery pause mode so predbat will set the inverter discharge rate to zero. You can see this in the predbat log:

2025-11-18 13:00:26.309331: Set inverter 0 pause mode PauseDischarge via REST successful on retry 0
2025-11-18 13:00:26.309363: Inverter 0 set pause mode to PauseDischarge
2025-11-18 13:00:26.309378: Disabling discharge during charge due to set_discharge_during_charge being False

At the start of the charge Predbat also turned off scheduled export, set the start & end times to 00:00:00 and set target SoC:

2025-11-18 13:00:26.309467: Inverter 0 Turning off scheduled export
2025-11-18 13:00:26.309481: Inverter 0 Adjust force export to False, change times from 06:30:00 - 07:01:00 to None - None
2025-11-18 13:00:26.309494: Adjust idle time, charge 13:00:00-16:00:00 discharge 00:00:00-00:00:00
2025-11-18 13:00:26.309674: Adjust demand (idle) time computed is 16:00:00-23:59:00
2025-11-18 13:00:26.309779: Setting charging SOC to 89 as per target
2025-11-18 13:00:26.309823: Inverter 0 adjust target soc for charge to 89% (12.19kWh/13.696kWh 9.52kWh) based on going from 75% -> 75% total add is 1.9kWh and this battery needs to add 0.95kWh to get to 7.14kWh
2025-11-18 13:00:26.309846: Inverter 0 Current Target SOC is 75%, already at target
2025-11-18 13:00:26.310249: Inverter 0 Current reserve is 4.0 already at target

At the start of the freeze charge, Predbat also cleared scheduled export times but doesn't turn off inverter discharge nor sets the target soc:

2025-11-18 14:20:05.787537: Inverter 0 Freeze charging with soc 86%
2025-11-18 14:20:05.787563: No export window planned
2025-11-18 14:20:05.787602: Inverter 0 write_and_poll_switch: No write needed for scheduled_discharge_enable as False == False
2025-11-18 14:20:05.787614: Inverter 0 Turning off scheduled export
2025-11-18 14:20:05.787628: Inverter 0 Adjust force export to False, change times from 06:30:00 - 07:01:00 to None - None
2025-11-18 14:20:05.787638: Adjust idle time, charge 00:00:00-00:00:00 discharge 00:00:00-00:00:00
2025-11-18 14:20:05.787727: Adjust demand (idle) time computed is 14:00:00-23:59:00
2025-11-18 14:20:05.787876: Not setting charging SoC as we are not within the window (now 11-18 14:20:00 target set_soc_minutes 30 charge start time 11-18 14:00:00)
2025-11-18 14:20:05.787988: Inverter 0 Current reserve is 4.0 already at target

At this point I'm leaning towards it either being the setting of target SoC or more likely the setting of battery discharge rate to 0 for a charge but not a freeze charge that is the problem for you.

gcoan avatar Nov 18 '25 20:11 gcoan

Thank you Geoffrey @gcoan for your response - greatly appreciated. I totally agree with your suggestion about the binary sensor. Rob's @raldred hint about the Predbat Status got me to thinking. As a result I decided to change the way to approach this.

We'll ditch the freeze charge call from apps.yaml and instead respond to changes in the Predbat Status - thanks Rob!

None of this will work without the LuxPower refresh interval being setup as per the LuxPower integration readme. We need the refresh interval to be as short as possible - 20 seconds is the recommended minimum. (Geoffrey, the flow charts and the 2 entities used in the binary sensor are both updated by the local connection to the inverter. The LuxPower servers are not involved.)

The Charge First / Charge Priority start and end times need to be set to 00:00 and 23:59 respectively as per this image.

Image

I have this in apps.yaml

support_charge_freeze: True

Template sensor of Binary type with 'show as' Power to monitor the difference between solar power and home consumption. Operator <= will produce ON when solar <= house load, OFF otherwise. Binary sensor

binary_sensor.solar_compare_home

{% if (states('sensor.lux_solar_output_live') | float(0)) <=
      (states('sensor.lux_home_consumption_live') | float(0)) %}
  on
{% else %}
  off
{% endif %}

This is the automation to respond to changes in the Predbat Status. It will set the initial state of the Charge Priority switch according to the ON or OFF status of the binary sensor. It also sets the battery rate and turns on the Freeze Charge running automation.

Automation for when Predbat Status changes to Freeze charging.

alias: Freeze charge start
description: >
  Set charge priority depending on solar/home comparison when Freeze or Hold charging begins.
  Then enable the freeze/hold running automation.
triggers:
  - entity_id: predbat.status
    to: "Freeze charging"
    trigger: state

conditions: []

actions:
  - if:
      - condition: state
        entity_id: binary_sensor.solar_compare_home
        state: "on"
    then:
      - action: switch.turn_on
        target:
          entity_id: switch.lux_charge_priority
      - action: input_number.set_value
        data:
          value: 0
        target:
          entity_id: input_number.battery_rate_max
    else:
      - action: switch.turn_off
        target:
          entity_id: switch.lux_charge_priority
      - action: input_number.set_value
        data:
          value: 4000
        target:
          entity_id: input_number.battery_rate_max

  - action: automation.turn_on
    target:
      entity_id: automation.freeze_charge_running

mode: single

This rewritten Freeze charge running automation will respond to any subsequent changes in the binary sensor. It will also check that we are actually Freeze charging.

Automation for when freeze charge is running.

alias: Freeze charge running
description: >
  Turn charge priority on/off depending on solar vs home usage while Freeze charging.
triggers:
  - entity_id: binary_sensor.solar_compare_home
    trigger: state

conditions:
  - condition: state
    entity_id: predbat.status
    state: "Freeze charging"

actions:
  - choose:

      # --- Solar > Home (turn ON if currently OFF) ---
      - conditions:
          - condition: state
            entity_id: binary_sensor.solar_compare_home
            state: "on"
          - condition: state
            entity_id: switch.lux_charge_priority
            state: "off"
        sequence:
          - action: switch.turn_on
            target:
              entity_id: switch.lux_charge_priority
          - action: input_number.set_value
            data:
              value: 0
            target:
              entity_id: input_number.battery_rate_max

      # --- Home > Solar (turn OFF if currently ON) ---
      - conditions:
          - condition: state
            entity_id: binary_sensor.solar_compare_home
            state: "off"
          - condition: state
            entity_id: switch.lux_charge_priority
            state: "on"
        sequence:
          - action: switch.turn_off
            target:
              entity_id: switch.lux_charge_priority
          - action: input_number.set_value
            data:
              value: 4000
            target:
              entity_id: input_number.battery_rate_max

mode: single

We need another automation to turn off the Freeze Charge running automation (we don't want it randomly firing throughout the day!) and also turn off the Charge Priority switch, if it is ON, and reset the battery rate when the Freeze charging slot ends.

Freeze charging slot ending automation.

alias: Freeze charge end
description: >
  Turn off freeze charging automation and turn off charge priority switch and
  reset battery rate if required.
triggers:
  - entity_id: predbat.status
    to: "Demand"
    trigger: state
  - entity_id: predbat.status
    to: "Charging"
    trigger: state
  - entity_id: predbat.status
    to: "Hold charging"
    trigger: state
  - entity_id: predbat.status
    to: "Discharging"
    trigger: state
  - entity_id: predbat.status
    to: "Freeze exporting"
    trigger: state

conditions: []

actions:
  # Turn off freeze-running automation
  - action: automation.turn_off
    data:
      stop_actions: true
    target:
      entity_id: automation.freeze_charge_running

  # Only reset things if charge priority is currently ON
  - if:
      - condition: state
        entity_id: switch.lux_charge_priority
        state: "on"
    then:
      - action: switch.turn_off
        target:
          entity_id: switch.lux_charge_priority
      - action: input_number.set_value
        data:
          value: 4000
        target:
          entity_id: input_number.battery_rate_max

mode: single

I tested this as much as I could and as far as I could tell the automations were running properly and all of the switching of Charge Priority was working as expected. However we still had the ongoing problem. It was naive of me to think that I could manipulate the battery_rate_max by changing the value in the apps.yaml because it is not read 'live'. However if we could somehow manipulate the discharge rate in a similar way, then there is a possibility that this could work.

I then tried a different approach again, this time manipulation the Charge Priority charge % level.

```alias: Freeze charge start
description: >
  Set charge priority depending on solar/home comparison when Freeze or Hold
  charging begins. Then enable the freeze/hold running automation.

triggers:
  - entity_id: predbat.status
    to: "Freeze charging"
    trigger: state

conditions: []

actions:
  - action: automation.turn_on
    target:
      entity_id: automation.freeze_charge_running

  - action: switch.turn_on
    target:
      entity_id: switch.lux_charge_priority

  - choose:
      - conditions:
          - condition: state
            entity_id: binary_sensor.solar_compare_home
            state: "on"
        sequence:
          - action: number.set_value
            target:
              entity_id: number.lux_priority_charge_level
            data:
              value: 100

      - conditions:
          - condition: state
            entity_id: binary_sensor.solar_compare_home
            state: "off"
        sequence:
          - action: number.set_value
            target:
              entity_id: number.lux_priority_charge_level
            data:
              value: "{{ states('sensor.lux_battery_soc_corrected') | float }}"

mode: single
alias: Freeze charge running
description: >
  Predbat charge_freeze_service switching. Turn on Charge Priority if Home
  Consumption is greater than Solar Output.

triggers:
  - entity_id: binary_sensor.solar_compare_home
    trigger: state

conditions:
  - condition: state
    entity_id: predbat.status
    state:
      - "Freeze charging"

actions:
  - choose:
      - conditions:
          - condition: state
            entity_id: binary_sensor.solar_compare_home
            state: "on"
        sequence:
          - action: number.set_value
            target:
              entity_id: number.lux_priority_charge_level
            data:
              value: 100

      - conditions:
          - condition: state
            entity_id: binary_sensor.solar_compare_home
            state: "off"
        sequence:
          - action: number.set_value
            target:
              entity_id: number.lux_priority_charge_level
            data:
              value: "{{ states('sensor.lux_battery_soc_corrected') | float }}"

mode: single
alias: Freeze charge end
description: >
  Turn off freeze/hold charging automation and turn off charge priority switch
  and reset battery rate if required
triggers:
  - entity_id: predbat.status
    to: "Demand"
    trigger: state
  - entity_id: predbat.status
    to: "Charging"
    trigger: state
  - entity_id: predbat.status
    to: "Hold charging"
    trigger: state
  - entity_id: predbat.status
    to: "Discharging"
    trigger: state
  - entity_id: predbat.status
    to: "Freeze exporting"
    trigger: state

conditions: []

actions:
  - action: automation.turn_off
    data:
      stop_actions: true
    target:
      entity_id: automation.freeze_charge_running

  - action: switch.turn_off
    target:
      entity_id: switch.lux_charge_priority

  - action: number.set_value
    data:
      value: 100
    target:
      entity_id: number.lux_priority_charge_level

mode: single

As with the previous approach this all worked but of course didn't have the desired effect. If there was a way to manipulate the target SoC, if that is what is causing the problem, then this too may well work.

Here's a snapshot of the Predbat Status this morning

Image

and one of the binary sensor

Image

Before the freeze charge starts we have this

2025-11-22 10:38:44.432907: Inverter 0 SOC: 7.77kW 77% Current charge rate 4000W Current discharge rate 4000W Current battery power 0.0W Current battery voltage 53.4V Grid power 0.0W load power 74.0W PV Power 146.0W

and when we are setting up for freeze charging we get this (note the first line at 10:39:27)

2025-11-22 10:39:27.433104: Inverter 0 current discharge rate is 4000W and new target is 0W
2025-11-22 10:39:37.521369: Inverter 0 write_and_poll_value: Wrote 0 to discharge_rate, successfully now 0.0
2025-11-22 10:39:48.256804: Inverter 0 write_and_poll_value: Wrote 0.0 to timed_discharge_current, successfully now 0.0
2025-11-22 10:39:48.257057: Inverter 0 Freeze charging with soc 77%
2025-11-22 10:39:48.257238: Next export window will be: 2025-11-22 23:30:00+00:00 - 2025-11-23 00:01:00+00:00 at reserve 7
2025-11-22 10:39:48.257422: Not setting export as we are not yet within the export window - next time is 11-22 23:30:00 - 11-23 00:00:00
2025-11-22 10:39:48.257655: Inverter 0 Adjust force export to False, change times from 08:00:00 - 16:01:00 to None - None
2025-11-22 10:39:48.257734: Adjust idle time, charge 10:30:00-11:00:00 discharge 00:00:00-00:00:00
2025-11-22 10:39:48.258296: Adjust demand (idle) time computed is 11:00:00-23:59:00
2025-11-22 10:39:48.258606: Within charge freeze setting target soc to current soc 77
2025-11-22 10:39:48.258732: Inverter 0 adjust target soc for hold to 77% based on requested all inverter soc 77%
2025-11-22 10:39:48.258865: Inverter 0 Current charge limit is 5% and new target is 77%
2025-11-22 10:39:58.973665: Inverter 0 write_and_poll_value: Wrote 77 to charge_limit, successfully now 77.0
2025-11-22 10:39:58.973981: Inverter 0 Skipped service discharge_stop_service domain discharge service_name switch.turn_off as it was previously called.
2025-11-22 10:39:58.974030: Inverter 0 Skipped service discharge_stop_service domain discharge service_name switch.turn_off as it was previously called.
2025-11-22 10:39:58.974090: Inverter 0 Calling service charge_start_service domain charge service_name switch/turn_on with data {'entity_id': 'switch.lux_ac_charge_enable'}
2025-11-22 10:39:59.677572: Inverter 0 Skipped service discharge_stop_service domain discharge service_name switch.turn_off as it was previously called.
2025-11-22 10:39:59.677813: Inverter 0 Skipped service discharge_stop_service domain discharge service_name switch.turn_off as it was previously called.
2025-11-22 10:39:59.678540: Inverter 0 count register writes 5
2025-11-22 10:39:59.866126: Total inverter register writes now 14944
2025-11-22 10:39:59.997326: Completed run status Freeze charging
2025-11-22 10:40:01.722173: Info: record_status Freeze charging target 77%

it is interesting that when Predbat is setting up for the Hold charging at 10:45:55 we get the discharge rate set back to 4000. I did try to look at what happens when Hold charging but it's hard to do because you can't force Predbat in Hold charging mode. I did have a Hold charging slot after the sun had set and the inverter was behaving as if Charge Priority was ON.

2025-11-22 10:45:07.299796: Inverter 0 current discharge rate is 0W and new target is 4000W

I've come to the conclusion that unless there is a way to make Predbat change the discharge rate and/or the target SoC during a freeze charge slot (if indeed that is the problem), we are doomed to failure, as far as being able to mimic the behaviour of Freeze charging as per the documentation.

If we can't change what Predbat does we have the imperfect option of just letting Predbat do what it is doing now - after all it's only different from the documentation during the daytime when Solar>House. Alternatively we could have an automation to turn the Set Freeze charge switch off during the hours of daylight.

Richard

brickatius avatar Nov 23 '25 12:11 brickatius

I have just realised that the target SoC CAN be adjusted by setting the system charge soc limit.

 - action: number.set_value
    target:
      entity_id: number.lux_system_charge_soc_limit
    data:
      value: 100

or

 - action: number.set_value
            target:
              entity_id: number.lux_system_charge_soc_limit
            data:
              value: "{{ states('sensor.lux_battery_soc_corrected') | float }}"
Inverter 0 adjust target soc for hold to 39% based on requested all inverter soc 39%

I will modify the second set of automations and report back when the sun is shining. (I know my logic was adrift - SoC should be 100 when OFF on the binary sensor vice versa).

brickatius avatar Nov 23 '25 15:11 brickatius

EUREKA! THE AIM To get a LuxPower inverter to mimic the behaviour of a GE inverter when Freeze Charging as described here.

Freeze charging - The battery is charging but the current battery level (SoC) is frozen (held). Think of it as a charge to the current battery level. The grid or solar covers any house load. If there is a shortfall of Solar power to meet house load, the excess house load is met from grid import, but if there is excess Solar power above the house load, the excess solar will be used to charge the battery,

This reads just like Demand mode but with any house load, which might have come from the battery, coming from the grid instead.

I've had one of those lightbulb moments. Looking at the logs, I had been wondering why the AC Charge switch was being turned ON when Predbat status was freeze charging. In a moment of inspiration, I looked at the LuxPower web portal and saw that when freeze charging, the AC Charge switch was enabled and crucially that the Stop AC Charge SOC(%) was the same as the present SoC of the battery. I realised that all we needed to do was turn the AC charge switch off when Solar power > House load (binary sensor Solar compare Home OFF) and the inverter would behave as we want it to. That would then satisfy the conditions of the Freeze Charging documentation (subject to the caveat mentioned at the end of this comment).

IMPLEMENTATION

Assumptions: a) The inverter refresh interval has been set up according to the instructions in the LuxPower integration readme. b The refresh interval has been set to 20 seconds. c) Lux SoC corrected sensor has been created in accordance with the Predbat LuxPower inverter setup documentation.

Requirements: A) Binary Sensor

binary_sensor.solar_compare_home

{{ 'on' if states('sensor.lux_solar_output_live') | float(0)
        <= states('sensor.lux_home_consumption_live') | float(0)
     else 'off' }}

B) Input boolean

input_boolean.freeze_charge_control

C) Freeze charge automation Debugging is enabled for evaluation purposes. I've included some annotations to best understand the logic and flow of the automation. Both of these can be removed later.

  1. Triggers written as templates rather than 'states'. Ensures robustness especially with two word trigger states.
  2. Conditions also written as templates are more robust.
  3. Debug logging written for evaluation purposes - can be removed later.
  4. The input boolean ensures that non of the actions can be executed (CLEANUP in particular) once the Predbat Status changes away from Freeze charging
  5. INITIALISATION a) It was noticed that 'charge last' was left on if doing a Manual Freeze Charge during a Freeze export slot - turn it off if required b) It was also noted that there was an unacceptable period of charging between the time Predbat turned the AC Charge switch to ON at the start of a Freeze charge slot and the Stop AC Charge SOC(%) value being set. Setting it now avoids that delay.
  6. CASE 1 - HOUSE LOAD > SOLAR POWER a) Respond to the ON state of the binary sensor by ensuring that the AC Charge switch is set to ON (if it isn't already) b) Avoid any unnecessary switching. c) When the switch is ON and the battery charge level has been set all of the house load is met from the GRID
  7. CASE 2 HOUSE LOAD < SOLAR POWER a) Respond when the state of the binary sensor changes to OFF (house < solar). b) Avoid any unnecessary switching. c) Run a loop every 20 seconds because every time Predbat recalculates during a freeze charge slot it will reset the AC Charge to switch to ON - we don't want this during an OFF period. Looping every 20 seconds will ensure a rapid response to this change d) The switch will only be turned ON if it is OFF. No unnecessary calls to the inverter.
  8. CLEANUP a) Turn OFF the AC Charge switch if it is already ON except if the next slot happens to be Hold charging or Charging, in which case leave it on. b) Finally turn off the input boolean so that none of the actions in the cleanup section can be repeated. Additionally this will ensure that none of the actions in this automation will take place until the next time Predbat is
alias: Freeze charge
description: >
  Optimised freeze charging automation using binary sensor only. Includes: safe
  switching, robust startup, stable freeze loop, clean shutdown, and full
  structured debug logging.
   
# 1) 
triggers:
  - trigger: template
    value_template: "{{ states('predbat.status') }}"
  - trigger: template
    value_template: "{{ states('binary_sensor.solar_compare_home') }}"    
# 2)
conditions:
  - condition: template
    value_template: "{{ is_state('predbat.status', 'Freeze charging') }}" 
# 3) 
actions:
  - action: system_log.write
    data:
      level: debug
      message: >
        FreezeCharge START: predbat.status={{ states('predbat.status') }},
        solar={{ states('binary_sensor.solar_compare_home') }}, ac_charge={{
        states('switch.lux_ac_charge_enable') }}, charge_last={{
        states('switch.lux_charge_last') }}.
# 4) 
  - action: input_boolean.turn_on
    target:
      entity_id: input_boolean.freeze_charge_control
# 5)
  - choose:
      - conditions:
# a)      
          - condition: template
            value_template: "{{ is_state('switch.lux_charge_last', 'on') }}"
        sequence:
          - action: system_log.write
            data:
              level: debug
              message: "FreezeCharge: Turning OFF charge_last (was ON)."
          - action: switch.turn_off
            target:
              entity_id: switch.lux_charge_last
# b) 
          - action: number.set_value
            target:
              entity_id: number.lux_ac_battery_charge_level
            data:
              value: "{{ states('sensor.lux_battery_soc_corrected') }}"
  - action: system_log.write
    data:
      level: debug
      message: "FreezeCharge: Evaluating solar_compare_home..."
 
  - choose:
# 6) CASE 1)    
      - conditions:
          - condition: template
            value_template: "{{ is_state('binary_sensor.solar_compare_home', 'on') }}"
        sequence:
          - action: system_log.write
            data:
              level: debug
              message: "FreezeCharge CASE 1: solar=ON → AC charge ON."
          - choose:
              - conditions:
                  - condition: template
                    value_template: "{{ is_state('switch.lux_ac_charge_enable', 'off') }}"
                sequence:
                  - action: system_log.write
                    data:
                      level: debug
                      message: "FreezeCharge: Turning ON AC charge."
                  - action: switch.turn_on
                    target:
                      entity_id: switch.lux_ac_charge_enable
            default:
              - action: system_log.write
                data:
                  level: debug
                  message: "FreezeCharge: AC charge already ON."
# 7a & b) CASE 2
                   
      - conditions:
          - condition: template
            value_template: "{{ is_state('binary_sensor.solar_compare_home', 'off') }}"
        sequence:
          - action: system_log.write
            data:
              level: debug
              message: "FreezeCharge CASE 2: solar=OFF → AC charge OFF + LOOP."
          - choose:
              - conditions:
                  - condition: template
                    value_template: "{{ is_state('switch.lux_ac_charge_enable', 'on') }}"
                sequence:
                  - action: system_log.write
                    data:
                      level: debug
                      message: "FreezeCharge: Turning OFF AC charge."
                  - action: switch.turn_off
                    target:
                      entity_id: switch.lux_ac_charge_enable
            default:
              - action: system_log.write
                data:
                  level: debug
                  message: "FreezeCharge: AC charge already OFF."
                  
# 7c) - LOOP

          - repeat:
              while:
                - condition: template
                  value_template: "{{ is_state('predbat.status', 'Freeze charging') }}"
                - condition: template
                  value_template: "{{ is_state('binary_sensor.solar_compare_home', 'off') }}"
              sequence:
                - action: system_log.write
                  data:
                    level: debug
                    message: >
                      FreezeCharge LOOP: solar={{
                      states('binary_sensor.solar_compare_home') }},
                      ac_charge={{ states('switch.lux_ac_charge_enable') }}.
                - choose:
                    - conditions:
                        - condition: template
                          value_template: "{{ is_state('switch.lux_ac_charge_enable', 'on') }}"
                      sequence:
                        - action: system_log.write
                          data:
                            level: debug
                            message: "FreezeCharge LOOP: Turning OFF AC charge."
                        - action: switch.turn_off
                          target:
                            entity_id: switch.lux_ac_charge_enable
                  default:
                    - action: system_log.write
                      data:
                        level: debug
                        message: "FreezeCharge LOOP: AC charge already OFF."
                - delay: "00:00:20"
    default:
      - action: system_log.write
        data:
          level: error
          message: >
            FreezeCharge ERROR: Unexpected solar state {{
            states('binary_sensor.solar_compare_home') }}.

 # 8) CLEANUP
 
  - action: system_log.write
    data:
      level: debug
      message: >
        FreezeCharge CLEANUP: status={{ states('predbat.status') }}, solar={{
        states('binary_sensor.solar_compare_home') }}.
  - choose: 
      - conditions:
          - condition: template
            value_template: "{{ is_state('input_boolean.freeze_charge_control', 'on') }}"
        sequence:
          - choose:
              - conditions:
                  - condition: template
                    value_template: >
                      {{ states('predbat.status') not in ['Charging', 'Hold
                      charging'] }}
                sequence:
                  - choose:
                      - conditions:
                          - condition: template
                            value_template: >-
                              {{ is_state('switch.lux_ac_charge_enable', 'on')
                              }}
                        sequence:
                          - action: system_log.write
                            data:
                              level: debug
                              message: "FreezeCharge CLEANUP: Turning OFF AC charge."
                          - action: switch.turn_off
                            target:
                              entity_id: switch.lux_ac_charge_enable
                    default:
                      - action: system_log.write
                        data:
                          level: debug
                          message: "FreezeCharge CLEANUP: AC charge already OFF."
          - action: system_log.write
            data:
              level: debug
              message: "FreezeCharge CLEANUP: Turning OFF freeze_charge_control."
          - action: input_boolean.turn_off
            target:
              entity_id: input_boolean.freeze_charge_control
mode: restart

Conclusion

I believe that this is the best that can be achieved and is as close as possible as we can get to Freeze Charging. a) When Solar > Home, house load is met by the solar power with any excess going to charge the battery as described. The battery is not discharged. FrzChrg conditions met. b) When Solar < Home there is a shortfall of solar power to meet the house load. House load is met from the grid (see caveat). The battery is not discharged. FrzChrg main condition met. c) When there is no solar, all of the house load comes from the grid. The battery is not discharged. FrzChrg conditions met.

Caveat: The only time that this doesn't quite match the Freeze Charging description will be during daylight hours when there is insufficient solar power to meet the house load. In this scenario all of the house load will come from the grid, rather than being split between solar and grid. The trade-off is that all of the solar power will go to charging the battery, rather than contributing to the house load. Next time the plan is calculated the SoC of the battery might be higher than was expected but the plan will take this into consideration.

brickatius avatar Nov 29 '25 18:11 brickatius

Gosh, you have written an essay @brickatius, with I think 3 different approaches. I know I wrote a long reply to you which prompted this thought process.

I don't pretend I follow all of the twists and turns of this, am trying to follow along though. But will wait before updating the documentation until you are sure you have everything you want.

I have a few observations on the writeups above:

  1. (this is from option 1 you wrote up last week, and I know you have moved on, but I'll share it in case you want to revisit whether this is a route to success)

However we still had the ongoing problem. It was naive of me to think that I could manipulate the battery_rate_max by changing the value in the apps.yaml because it is not read 'live'. However if we could somehow manipulate the discharge rate in a similar way, then there is a possibility that this could work.

You are correct, battery_rate_max is read by predbat on startup and doesn't change live on each run as it was expected to be a limit you'd set once as part of initial config, not change dynamically. However, you can overwrite the value predbat is using by using the predbat manual API https://springfall2008.github.io/batpred/manual-api/

Here's part of an automation I use to throttle my battery charge rates during the summer so I trickle charge the batteries through the day and don't drive the grid voltage up so much at lunchtime by having full batteries and exporting all my solar:

action: select.select_option
alias: Trickle charge G, set charge rate to 1500W
target:
  entity_id:
    - select.predbat_manual_api
data:
  option: inverter_limit_charge(0)=1500
  1. This is from option 2, and I see you have moved away from it now, but worth saying I think:
triggers:
  - entity_id: predbat.status
    to: "Demand"
    trigger: state
  - entity_id: predbat.status
    to: "Charging"
    trigger: state

I would advise not doing a trigger like this because you have to precisely match the predbat status values and there are a lot of them, including some like Read-only that were missed out of the documentation, and if you have a car or an iBoost then you get things like ', Hold for car' added to the end of the status.

Instead I would trigger on any change of state of predbat.status and then have a condition to look for 'Freeze charge' which is pretty much the way you have done it in v3

  1. Its a style thing but I wouldn't use template triggers, I would use state triggers as its I think more readable
triggers:
  - trigger: template
    value_template: "{{ states('predbat.status') }}"
  - trigger: template
    value_template: "{{ states('binary_sensor.solar_compare_home') }}"  

becomes

triggers:
  - trigger: state
    entity_id: predbat.status
  - trigger: state
    entity_id: binary_sensor.solar_compare_home

so they fire on any change in state of the sensors and you don't need a template to be evaluated

  1. I like your debug logs, not seen that before in an automation. Every day a school day, thanks !

  2. Your condition is going to cause problems I think

conditions:
  - condition: template
    value_template: "{{ is_state('predbat.status', 'Freeze charging') }}" 

I get why you are using a template for this for multiple word statii, I think you could just do it with a state check with the right quotes around the state, but regardless of the style of that, this condition will stop the automation executing if the predbat status is not freeze charging so your cleanup logic can never be executed.

I think you need to shift this condition into an if-then-else in the actions part of the automation

(I can see that your loop causes the automation to keep running so maybe this issue is avoided, but I don't like that approach and there are other issues with it, see below)

  1. More templates!
          - condition: template
            value_template: "{{ is_state('switch.lux_charge_last', 'on') }}"

I would again just test the state:

condition: state
state:
  - "on"
entity_id: switch.lux_charge_last

I'm not sure what the template adds over a simple state check? It just seems easier to read and requires less Jinja execution for HA

  1. Point 7, your loop

7 CASE 2 HOUSE LOAD < SOLAR POWER a) Respond when the state of the binary sensor changes to OFF (house < solar). b) Avoid any unnecessary switching. c) Run a loop every 20 seconds because every time Predbat recalculates during a freeze charge slot it will reset the AC Charge to switch to ON - we don't want this during an OFF period. Looping every 20 seconds will ensure a rapid response to this change d) The switch will only be turned ON if it is OFF. No unnecessary calls to the inverter.

I don't like this, it feels the wrong way to do this, to let predbat change something and then your automation changes it back again.

Also, your solution relies on the automation running all the time, i.e. busy waiting. This is inefficient and I have a recollection (may be false) that there is maximum execution time for an automation, but moreover, automations don't survive restarts of HA, so when HA restarts your automation will stop running.

I have a better solution. Instead of predbat directly changing the lux AC charge switch, it changes a helper switch. You then have an automation that fires when the helper switch state changes and that decides based on predbat status whether to actually turn the lux AC charge switch on or not. So the helper acts as an intelligent proxy. Actually you will need to put a delay in the automation because predbat will set all the different inverter controls for the charge and then at the end it sets its status, so if your automation fires and has no delay then you'll get in a race condition and predbat status may or may not say 'freeze charging'

  1. More defensive handling of predbat statii

As I mentioned earlier predbat status can have things added to the end of it, so instead of:

                      {{ states('predbat.status') not in ['Charging', 'Hold
                      charging'] }}

you would be much safer to check whether predbat.status contains the word 'charging'

See the full list of statii from the code I have added to the documentation https://github.com/gcoan/batpred/blob/main/docs/what-does-predbat-do.md#predbat-status

Let me know what you think.

gcoan avatar Nov 29 '25 21:11 gcoan

Thanks Geoffrey. Your reply has pointed me in the right direction. You can basically ignore anything in the earlier comments apart from my last one. The way forward is the manipulation of the AC Charge switch. In light of your comments I think I need to go back to a 3 automation approach as I was doing before.

  1. First automation which turns on when 'charge_freeze_service' is called. This to establish a starting ON/Off state for the AC Charge switch depending on the binary sensor state. Then turn on a freeze charge running automation.
  2. Charge running automation triggered by a) changes in the binary sensor and, here's the important thing b) the ON/OFF state of the AC Charge switch. This would do away with the the need for a loop. AC Charge switch turned ON during this time by Predbat when it is supposed to be OFF - just turn it back on again. If it's already ON do nothing.
  3. Third automation switched on by the charge_stop_service with an input boolean. Cleans everything up, turns off the other 2 automations and hopefully will turn itself off at the end. This does away with any problems relating to HA restarts and doesn't leave anything running to get in the way of anything else. I'm happy to remove the templates for readability. There was one occasion where the automation didn't fire because the double quotation marks were missing. There have been so many iterations of the automation that I can't remember if I had omitted them or they somehow got removed which is why I went along the template route. Let me know if you are happy for me to proceed along these lines. Richard

brickatius avatar Nov 29 '25 22:11 brickatius

Hi Richard @brickatius that makes sense, and you don't need to get my OK on anything, I'm just giving feedback on what you've done so we can try to get to a simple solution as possible.

What you propose sounds fine, pretty simple, and above all, robust.

Cheers Geoffrey

gcoan avatar Nov 29 '25 22:11 gcoan