Skip to content

Expose solar surplus power and force-export window state as sensors - #3791

Open
Pezmc wants to merge 5 commits into
springfall2008:mainfrom
Pezmc:feature/car-charging-solar-surplus
Open

Expose solar surplus power and force-export window state as sensors#3791
Pezmc wants to merge 5 commits into
springfall2008:mainfrom
Pezmc:feature/car-charging-solar-surplus

Conversation

@Pezmc

@Pezmc Pezmc commented Apr 16, 2026

Copy link
Copy Markdown

Pared down to the two sensors @chalfontchubby asked for in this comment. Predbat decides nothing about solar car charging. The automation or the charger firmware does.

Rebuilt on current main rather than rebased. Most of the old diff was the decision logic that has gone.

The rework and its review pass were both Claude-assisted, so a sceptical eye on the arithmetic is welcome.

The two sensors

Entity Meaning
predbat.solar_surplus_power kW a load could take now without importing or draining the battery, totalled over all inverters. Nothing about it is car-specific
binary_sensor.predbat_force_export_slot on when the plan has a force export slot running, so the solar is worth more sold than put in the car
min(max(0, grid_power + car_charging_power - max(0, battery_power)), max(0, pv_power))

car_charging_power is added back because a charger inside the CT clamp sits behind the grid meter, so export collapses to zero once the car starts. Without it the sensor would drop to nothing the moment you acted on it. This uses the reading added in #4715, so there is no hysteresis or stored state.

The add-back is gated on car_energy_reported_load. With that switch Off the charger is outside the clamp, the grid reading never saw the car, and adding it back would invent a surplus equal to the whole charger draw. prediction.py branches on the same flag. The car_charging_power_included attribute says which applies.

Battery discharge is subtracted, so battery power is never reported as solar. When cloud arrives mid-charge the sensor falls to the real surplus instead of reporting one the battery is paying for. Battery charging is not added back, so a charging battery takes the solar first.

The result is capped at pv_power. Nothing can be spare that was never generated. A grid sensor wired positive-on-import without grid_power_invert would otherwise make the surplus track the import. The cap cannot fix the sign in daylight, but it bounds the error and forces 0 after dark.

Attributes carry every input: grid_power, battery_power, pv_power, car_charging_power, car_charging_power_configured and car_charging_power_included.

binary_sensor.predbat_force_export_slot comes from the plan, using the same condition publish_export_limit already uses. binary_sensor.predbat_exporting is the commanded state, so it is off in read-only mode, off during "Hold exporting", and only on for the minutes an export is actually being written. The plan-derived version stays on for the whole slot and covers freeze export.

Example automation

alias: Car charging on solar surplus
triggers:
  - trigger: numeric_state
    entity_id: predbat.solar_surplus_power
    above: 1.4
    for: "00:05:00"
  - trigger: numeric_state
    entity_id: predbat.solar_surplus_power
    below: 1.2
    for: "00:05:00"
  - trigger: state
    entity_id: binary_sensor.predbat_car_charging_slot
    to: "on"
  - trigger: state
    entity_id: binary_sensor.predbat_car_charging_slot
    to: "off"
  - trigger: state
    entity_id: binary_sensor.predbat_force_export_slot
    to: "on"
  - trigger: state
    entity_id: binary_sensor.predbat_force_export_slot
    to: "off"
actions:
  - choose:
      - conditions:
          - condition: or
            conditions:
              # Predbat planned this slot, so charge regardless of the sun
              - condition: state
                entity_id: binary_sensor.predbat_car_charging_slot
                state: "on"
              - condition: and
                conditions:
                  - condition: numeric_state
                    entity_id: predbat.solar_surplus_power
                    above: 1.2
                  - condition: state
                    entity_id: binary_sensor.predbat_force_export_slot
                    state: "off"
        sequence:
          <commands to turn on your car charger>
    default:
      <commands to turn off your car charger>
mode: single

1.4kW is 6A at 230V, the minimum most chargers take. The 1.2kW condition gives it something to sit in so passing cloud does not switch the charger repeatedly.

Every trigger is paired with a to: state and every condition reads the entity, not the trigger that fired. A bare state trigger fires when a slot ends as well as when it starts, and a trigger-id condition would then turn the charger on at the end of the cheap slot. Triggering on both edges of force_export_slot is what lets charging resume when an export slot finishes, because numeric_state only fires on a threshold crossing.

The docs also show setting charger amps from the surplus, and note that a charger with its own solar mode (Zappi Eco+, Hypervolt Super Eco, Ohme) only needs the sensors to switch that mode on and off.

Removed since the first version

Everything that made a decision: the car_charging_solar_surplus / _threshold / _limit config items, detect_car_solar_surplus(), the previous-cycle hysteresis, the carHolding and pause-discharge changes with their "Hold for car (solar)" status, the car_charging_slot override, and binary_sensor.predbat_car_charging_solar_surplus. No changes to config.py, fetch.py, predbat.py or the apps.yaml schema. No new switches.

One open question

The surplus sensor is published unconditionally, unlike predbat.car_charging_power which only appears when a charger is configured. With no charger it is the grid export clamped at zero, and car_charging_power_configured says whether the add-back is live. Happy to gate it the same way if you prefer.

Tests

apps/predbat/tests/test_solar_surplus.py, run with --test solar_surplus. Covers plain export, import clamping, the car add-back, the CT clamp gate both ways, the generation cap, battery discharge subtraction, battery charging, the unconfigured case, and the export slot sensor across in-window, freeze, idle limit, out-of-window, export control off and no windows. ./run_all --quick and ./run_pre_commit pass.

Docs: a "Charging from solar surplus" section in docs/car-charging.md covering the formula, the checks to make before driving a load from it, and both sensors in docs/output-data.md. One drive-by fix in there, the four current power sensors were documented as Watts but publish kW.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a core “solar surplus car charging” mode that reuses Predbat’s existing car-charging slot signaling to divert excess PV export into EV charging, with new config controls and observability.

Changes:

  • Introduces 3 new config entities to enable/shape surplus-charging behavior (master switch, W threshold, ignore SoC limit).
  • Updates execute_plan() to detect live surplus export with hysteresis and publish surplus-related binary sensors/attributes.
  • Extends execute/test infrastructure and documentation to cover the new feature.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
docs/car-charging.md Documents the new solar surplus charging feature, configuration, and sensors.
apps/predbat/config.py Adds three new configuration items for surplus charging.
apps/predbat/fetch.py Reads the new configuration items into runtime fields.
apps/predbat/execute.py Implements surplus detection, battery-discharge hold integration, and sensor publishing.
apps/predbat/predbat.py Initializes car_charging_solar_surplus_active in reset state.
apps/predbat/tests/test_infra.py Adds defaults/reset fields for the new config/state in tests.
apps/predbat/tests/test_execute.py Adds scenarios validating surplus activation/inhibition rules and status text.
.cspell/custom-dictionary-workspace.txt Adds “deadband” to spelling dictionary.

Comment thread apps/predbat/execute.py Outdated
Comment thread apps/predbat/execute.py Outdated
@Pezmc

Pezmc commented Apr 17, 2026

Copy link
Copy Markdown
Author

@springfall2008 I've addressed the copilot feedback, both suggestions were valid edge cases

@springfall2008

Copy link
Copy Markdown
Owner

Hi Pezmc,

One question I have, why is this hard-wired to inverter 0?

There's a bit cleanup required, in that reset() should be updated to reset the self.* attributes used in this code and then the getattr() can also be removed.

Do you want me to try to clean up or would you like to have a go?

@Pezmc

Pezmc commented Apr 19, 2026

Copy link
Copy Markdown
Author

One question I have, why is this hard-wired to inverter 0?

It's hard-wired to inverter.id == 0 solely so it only runs once (since it uses global state, not anything per-inverter). Following the pattern file e.g. the status_extra, for example.

Happy to move it up above the inverter loop instead if you'd prefer!

There's a bit cleanup required, in that reset() should be updated to reset the self.* attributes used in this code and then the getattr() can also be removed.

I've attempted to address, and applied a slight refactor to handle number of cars changing.


If you'd like further changes or want to make some tweaks, please have at it!

Thanks for taking the time!

@bpinto

bpinto commented Apr 19, 2026

Copy link
Copy Markdown
Contributor

Thanks for this work! I have been missing this and this will be an awesome addition.

My 2 cents for two of items being added:

Surplus detection: compares grid export power against the car's charge rate minus a configurable threshold (default 500W shortfall allowance)

It would be awesome if instead the car charge rate were to be adjusted so that the car would charge with the correct surplus. It would have a minimum/maximum car charge rate.

Ignore charge limit: optionally charges past the car's SoC target when using surplus (default: on, since the energy would otherwise be wasted)

Like soc min/recommended, it would be nice if we could define an upper limit that would be respected.

This way we could continue to set a SoC target for Predbat to manage while allowing any surplus to charge the car slightly more.

In short this would convert the Boolean (100%) value to a configurable integer.

@Pezmc

Pezmc commented Apr 20, 2026

Copy link
Copy Markdown
Author

@bpinto thanks, appreciate the thoughts!

It would be awesome if instead the car charge rate were to be adjusted

If your charger supports variable current, I think this is actually better handled in your own HA automation than inside Predbat. The automation already has to flip the charger on and off and can read solar_surplus flag. Reading the live grid export and setting the rate from the same place is a pretty natural extension.

One thing Predbat could usefully do is expose the surplus amount it's currently seeing as an attribute, so the automation doesn't have to recompute it. But actually modulating the rate from inside Predbat feels like feature creep to me, given Predbat can't actuate the charger directly anyway.

Like soc min/recommended, it would be nice if we could define an upper limit that would be respected.

Yeah, that one's fair, turning the boolean into a configurable cap makes sense. It's a boolean to KISS for now. @springfall2008 do you think we should make this change now, or handle as a separate issue?

@Pezmc

Pezmc commented Apr 21, 2026

Copy link
Copy Markdown
Author

Having sat on this overnight I'm going to change the boolean to a limit that defaults to 100%, to save needing to introduce backwards incompatible changes or having to maintain backwards compatibility going forward. Thanks for the suggestion @bpinto

@bpinto

bpinto commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

@Pezmc thanks for taking into consideration my comments! By no means, I think any of that is a blocker, but I think it's an extra/improvement.

If your charger supports variable current, I think this is actually better handled in your own HA automation than inside Predbat. The automation already has to flip the charger on and off and can read solar_surplus flag. Reading the live grid export and setting the rate from the same place is a pretty natural extension.

One thing Predbat could usefully do is expose the surplus amount it's currently seeing as an attribute, so the automation doesn't have to recompute it. But actually modulating the rate from inside Predbat feels like feature creep to me, given Predbat can't actuate the charger directly anyway.

I think it would make sense for predbat to be responsible for this since predbat does similar to battery/inverter settings. For instance, it automatically sets the SoC target for the batteries and can also configure the charging rate.

Many of predbat's input support either a fixed value or a sensor, I haven't read how input_number.predbat_car_charging_rate has been configured internally, but given the existing pattern, I think it would be okay to allow it to be configured to a sensor that can be dynamically updated and that would be used to set the charging rate.

That said, not everyone uses chargers that supports variable charging rate, so the ideas are not exclusive since they support a different group of users.

@Pezmc

Pezmc commented Apr 21, 2026

Copy link
Copy Markdown
Author

input_number.predbat_car_charging_rate is in fact the opposite, that's the user telling predbat the max charge rate of their car. We'd need to introduce a second property like input_number.predbat_current_car_charging_rate which could be controlled by the surplus export logic. Either way, let's get this out as it stands (with the changes I've agreed to above) and we can discuss the best way to go about making it easy for users to configure a variable charge rate in a separate issue.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.

Comment thread apps/predbat/execute.py Outdated
Comment thread apps/predbat/execute.py Outdated
Comment thread apps/predbat/tests/test_execute.py Outdated
Comment thread apps/predbat/config.py Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Comment thread apps/predbat/execute.py Outdated
@Pezmc

Pezmc commented Apr 21, 2026

Copy link
Copy Markdown
Author

@springfall2008 Updated to use input_number.predbat_car_charging_solar_surplus_limit instead of a boolean to control the surplus charging. Defaults to 100% (so always dump excess current into the car), but can be configured as user wishes.

This is ready for re-review and any tweaks you wish to make!

@Pezmc

Pezmc commented May 8, 2026

Copy link
Copy Markdown
Author

@springfall2008 I've been dog fooding this for a couple weeks and have added some additional test coverage and fixes based on that.

@ward0

ward0 commented May 9, 2026

Copy link
Copy Markdown

I follow @bpinto , predbat should export the data as sensors. Predbat is really for the inverter and battery.

Maybe a new 'app' called predcar could be born? But an option to default use predbat configuration as an default proposal/option?

Now I use predbat as EMS for ha automation to control the EV charge Amps based on PV surplus when battery is full (Huawei here...)

@Pezmc
Pezmc force-pushed the feature/car-charging-solar-surplus branch from 97a8dce to 41afe3d Compare June 1, 2026 08:43
@Pezmc

Pezmc commented Jun 1, 2026

Copy link
Copy Markdown
Author

Hi @springfall2008, just a friendly nudge, this has been open for about six weeks now and I've been running it on my own system without issues. Your earlier feedback has been addressed (reset cleanup, the limit replacing the boolean, hoisting out of the per-inverter loop) and tests are passing.

@bpinto @ward0 on the idea of exposing sensor data: the PR now includes a predbat.car_charging_solar_surplus_power numeric sensor that reports the effective surplus in kW, so automations can already use that to drive variable-rate charging or other loads. I think that covers the sensor-data side of things.

On the suggestion of a separate "predcar" project: I'd push back on that for now. Predbat already manages car charging schedules, tracks car SoC, and integrates with Octopus Intelligent. Solar surplus diversion is a natural extension of that existing functionality, not a separate concern. Splitting it out would mean duplicating all the inverter/rate/battery context that Predbat already has. I'd rather get this merged as-is and then discuss any further enhancements (like variable charge rate control) as follow-up issues.

Ready for re-review whenever you have a moment. Happy for you to make any tweaks you'd like directly on the branch.

Thanks!

@ward0

ward0 commented Jun 1, 2026

Copy link
Copy Markdown

@Pezmc I hope your PR gets active soon.
Let me know if you need feedback or you want feedback.
Ward

@zlakes01

zlakes01 commented Jul 9, 2026

Copy link
Copy Markdown

Anything new about this great PR?

@ward0

ward0 commented Aug 18, 2026

Copy link
Copy Markdown

@Pezmc Can we still use your 'code'? Because this PR is still open :(

@Pezmc

Pezmc commented Aug 18, 2026

Copy link
Copy Markdown
Author

@ward0 It seems like the PR is blocked by @springfall2008, so please indeed feel free to use as needed, with attribution.

If @springfall2008 (or another reviewer) picks this up, I'll take the time to rebase on main (which has moved forward), so it's ready to merge again.

@chalfontchubby

Copy link
Copy Markdown
Collaborator

Sorry for the long silence on this one, and thanks for the sustained work and patience @Pezmc.

Our hesitation isn't about whether solar-surplus car charging is useful - it clearly is, and the demand in this thread backs that up. It's that the actual "when do I switch the car on" decision varies a lot by charger: some (Hypervolt's Super Eco, Zappi's Eco+, Ohme's own solar matching, etc.) already do proper real-time current modulation in hardware/firmware once told to enter that mode - an automation flipping a switch beats Predbat toggling on/off every 5-minute cycle for those. Others are dumb on/off relays with no charger-side intelligence at all, where someone has to make the call externally. Charge rates, number of phases, and where people's grid/CT power sensors actually sit also all vary enough that we're wary of Predbat owning a decision that's this charger-specific - we don't want to duplicate, and inevitably diverge from, logic that's often better handled at the charger or in a user's own automation.

What we would welcome: the two pieces of state in here that are genuinely hard to get right outside Predbat, published as plain sensors, without the switch/threshold/hysteresis decision logic attached:

  • car_charging_solar_surplus_power - the effective surplus, already correctly aggregated across every inverter (grid_power/battery_power sum per-inverter in execute.py, not just a single sensor)
  • whether Predbat currently has a force-export window active, so an automation doesn't end up switching a car on right through a slot Predbat is actively trying to sell power from

Neither of those is exposed anywhere today. That gives every charger type - smart or dumb - the one input that's actually hard to compute externally, while leaving the on/off (or rate) decision to whichever automation or charger firmware is best placed to make it for that specific setup.

Happy to take another look if you'd like to pare this down to the sensor-only version - given the interest in this thread it seems worth landing something.

@Pezmc
Pezmc force-pushed the feature/car-charging-solar-surplus branch from 41afe3d to b4ba66b Compare August 25, 2026 17:34
@Pezmc Pezmc changed the title Add solar surplus car charging to divert excess PV to EV Expose solar surplus power and force-export window state as sensors Aug 25, 2026
@chalfontchubby

Copy link
Copy Markdown
Collaborator

Thanks - I'll look more in a bit - one observation (which is more churn I could have considered earlier) is that the solar excess now isn't specifically for the car - anything could use it - so perhaps we can remove "car" from the sensor name.

Pezmc added 4 commits August 25, 2026 18:15
predbat.solar_surplus_power reports the power available for a flexible load
right now, aggregated over every inverter, so an automation can run a car
charger, an immersion heater or anything else on spare solar without Predbat
deciding anything.

A car charger sits behind the grid meter, so once a car is charging the export
collapses towards zero. car_charging_power is added back to recover what the
export would be with the car off, which keeps the reading stable enough to drive
a charger from - any other load can subtract the car_charging_power attribute if
it wants the car's share left out. Battery discharge is subtracted so battery
power is never offered as if it were solar: if cloud cover arrives mid-charge
the sensor falls to the real surplus instead of reporting one the battery is
paying for.

The components are published as attributes so users can build their own variant,
including a battery-versus-load priority, which is policy rather than something
Predbat should pick.
binary_sensor.predbat_force_export_slot says whether the plan has a force export
window running now, so an automation charging a car on solar surplus can leave
the solar for Predbat to sell instead.

It is taken from the plan rather than from the commanded state behind
binary_sensor.predbat_exporting, which is off in read only mode, off during Hold
exporting and only on for the minutes an export is actually being commanded. The
plan-derived version stays on right through a slot and covers freeze export,
where the battery is held and the PV is what gets sold.
predbat.load_power, battery_power, pv_power and grid_power are published in kW
(publish_inverter_data divides by 1000 and sets unit_of_measurement kW) but the
documentation called them Watts. Record the sign conventions while here.
Adds a 'Charging from solar surplus' section to the car charging docs covering
the formula, the attributes, an example automation with on/off thresholds and a
proportional-amps variant, and a table separating live surplus from the
predicted export triggers and the commanded exporting sensor.
@Pezmc
Pezmc force-pushed the feature/car-charging-solar-surplus branch from b4ba66b to 2172347 Compare August 25, 2026 18:19
Two ways the sensor could report a surplus that was not there.

A charger outside the CT clamp is never seen by the inverter's grid reading, so
adding car_charging_power back invented a surplus equal to the whole charger
draw. The add-back is now gated on car_energy_reported_load, the flag
prediction.py already uses for this. car_charging_power_included says which
applies.

The result is capped at pv_power. A grid sensor wired positive-on-import
without grid_power_invert made the surplus track the import instead, holding a
charger at full rate. The cap cannot fix the sign in daylight, but it bounds
the error and forces 0 after dark.

The documented automation had three faults: a bare state trigger on
car_charging_slot turned the charger on when the cheap slot ended, charging
could not resume after a force export slot because numeric_state only fires on
a crossing, and the modulating-current snippet floored at 6A with no off state.

Docs now also cover the PV inventory and freshness checks, and correct the
update interval to INVERTER_QUICK_UPDATE_SECONDS.
@Pezmc

Pezmc commented Aug 26, 2026

Copy link
Copy Markdown
Author

@chalfontchubby That's done and I've adjusted the name and the docs say it isn't car-specific.

While renaming I ran another check and spotted:

  • The car add-back ignored car_energy_reported_load. With that Off the charger is outside the CT clamp, so the grid reading never saw the car, and adding it back invented a surplus equal to the whole charger draw. This now reads the flag, same as prediction.py
  • Nothing was protecting against bad input from generation, a sensor wired positive-on-import would have made the surplus. This is now capped at pv_power now.

So it's now:

min(max(0, grid_power + car_charging_power - max(0, battery_power)), max(0, pv_power))

Of note, the rework were both Claude-assisted, so a sceptical eye on the math in particular would be welcome.

EDIT: Feel free to make any additional commits or tweaks needed to get this merged

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants