Show car charging on the web power flow diagram - #4715
Merged
Conversation
Adds car_charging_power, an optional apps.yaml sensor giving the live power (W) drawn by an EV charger. It is display only - the plan still models car charging from car_charging_energy and is unaffected by this. When it is set: - the web interface power flow diagram gains a Car alongside PV, battery and grid, animated while charging and dashed when idle - the House figure becomes load_power minus the car, clamped at zero, so it reads as the rest of the household rather than counting the charger twice - predbat.car_charging_power (kW, device_class power) is published for upstream consumers. It is only published when a charger is configured, so its absence distinguishes "no charger" from "charger idle" All five components that already wire car_charging_energy now wire the matching power sensor from the same device list, so entry N is the same charger: Ohme, myenergi Zappi, GECloud EV charger, AlphaESS and the gateway. Ohme's is tied to the same decision as its energy sensor - when a third party charger owns the energy figure, Ohme's own power reading is left out rather than reported beside it. The apps.yaml templates ship a regular expression matching Wallbox and Zappi, as car_charging_energy does. Three cases the reader handles deliberately, each covered by a test that failed first: - a charger reporting 'unavailable' with nothing plugged in reads as zero without flagging the run as errored, which get_arg's numeric path would otherwise do and leave Predbat in "with Errors" all day - an unresolved "re:" template default counts as not configured, so a house with no charger never gets a Car drawn before auto_config(final) runs - each entity is resolved individually rather than by index, as auto_config() leaves a None in place of a list entry whose expression found nothing and an index walk would stop at the hole instead of the chargers after it Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Validation mishandles transient unavailable states, stale published sensors are not cleaned up, and the transition test does not cover shared publication state.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds optional live EV-charger power monitoring to Predbat’s web power-flow diagram and publishes it as a kW sensor without changing charging-plan calculations.
Changes:
- Reads, aggregates, converts, and publishes charger power.
- Adds a Car diagram node and subtracts charging power from household load.
- Adds integration mappings, templates, documentation, and tests.
File summaries
| File | Summary |
|---|---|
templates/teslemetry.yaml |
Adds charger-power configuration. |
templates/tesla_powerwall.yaml |
Adds charger-power configuration. |
templates/sunsynk.yaml |
Adds charger-power configuration. |
templates/solis_cloud.yaml |
Adds charger-power configuration. |
templates/solax_sx4.yaml |
Adds charger-power configuration. |
templates/solax_cloud.yaml |
Adds charger-power configuration. |
templates/solaredge.yaml |
Adds charger-power configuration. |
templates/solar_assistant_growatt_sph.yaml |
Adds charger-power configuration. |
templates/solar_assistant_growatt_spa.yaml |
Adds charger-power configuration. |
templates/sofar.yaml |
Adds charger-power configuration. |
templates/sofar_modbus.yaml |
Adds charger-power configuration. |
templates/sigenergy_sigenstor.yaml |
Adds charger-power configuration. |
templates/sigenergy_cloud.yaml |
Adds charger-power configuration. |
templates/luxpower.yaml |
Adds charger-power configuration. |
templates/kostal.yaml |
Adds a charger-specific placeholder. |
templates/huawei.yaml |
Adds a charger-specific placeholder. |
templates/hanchu_cloud.yaml |
Adds charger-power configuration. |
templates/givenergy_givtcp.yaml |
Adds charger-power configuration. |
templates/givenergy_ems.yaml |
Adds charger-power configuration. |
templates/givenergy_cloud.yaml |
Adds charger-power configuration. |
templates/ginlong_solis.yaml |
Adds charger-power configuration. |
templates/ge_cloud_octopus_standalone.yaml |
Adds charger-power configuration. |
templates/fronius.yaml |
Adds charger-power configuration. |
templates/fox.yaml |
Adds charger-power configuration. |
templates/fox_cloud.yaml |
Adds charger-power configuration. |
templates/ep_cube_cloud.yaml |
Adds charger-power configuration. |
templates/enphase_cloud.yaml |
Adds charger-power configuration. |
templates/alphaess_cloud.yaml |
Adds charger-power configuration. |
docs/web-interface.md |
Documents the diagram changes. |
docs/output-data.md |
Documents the published sensor. |
docs/car-charging.md |
Documents charger-power behavior. |
docs/apps-yaml.md |
Documents the configuration option. |
apps/predbat/web.py |
Renders the Car power-flow node. |
apps/predbat/unit_test.py |
Registers the new tests. |
apps/predbat/tests/test_web_power_flow.py |
Tests charger reading and diagram behavior. |
apps/predbat/tests/test_ohme.py |
Tests Ohme mapping. |
apps/predbat/tests/test_myenergi.py |
Tests Zappi mapping. |
apps/predbat/tests/test_ge_cloud.py |
Tests GECloud mapping. |
apps/predbat/tests/test_gateway.py |
Tests Gateway mapping. |
apps/predbat/tests/test_alphaess_publish.py |
Tests AlphaESS mapping. |
apps/predbat/predbat.py |
Initializes charger-power state. |
apps/predbat/ohme.py |
Maps Ohme power sensors. |
apps/predbat/myenergi.py |
Maps Zappi power sensors. |
apps/predbat/gecloud.py |
Maps GECloud power sensors. |
apps/predbat/gateway.py |
Maps Gateway power sensors. |
apps/predbat/execute.py |
Reads and publishes charger power. |
apps/predbat/config.py |
Adds charger-power configuration schema. |
apps/predbat/alphaess.py |
Maps AlphaESS power sensors. |
Review details
Suppressed comments (3)
apps/predbat/execute.py:1075
- This flag is set before the sensor loop and before
self.car_charging_poweris assigned at line 1090. The WebInterface runs in a separate thread and reads these two fields independently (web.py:591-592), so while a HA read is in progress it can render a configured Car using the previous cycle's power whileload_poweris already current. Compute and publish a consistent snapshot, or synchronize the paired reads/writes.
self.car_charging_power_configured = True
apps/predbat/ohme.py:486
- The branch is skipped for any truthy existing energy argument, including the valid explicit
sensor.predbat_ohme_energy_today. In that configurationset_arg_auto("car_charging_power", POWER_WATTS_ENTITY)is never reached, so the same Ohme charger does not get automatic power wiring and the new display/sensor is absent unless power is configured separately. Distinguish the Ohme-owned energy entity (including the supported list form) from third-party energy sensors, and add a regression case.
# Live charge power, for the web power flow diagram and the predbat.car_charging_power
# sensor. Deliberately tied to the same decision as the energy sensor above: the two
# have to describe the same charger, so when another charger owns the energy figure
# Ohme's own power reading is left out rather than reported beside it.
self.set_arg_auto("car_charging_power", POWER_WATTS_ENTITY)
docs/output-data.md:290
- This says the sensor is published only when the key is set in
apps.yaml, but the supported Ohme, myenergi, GECloud, AlphaESS and Gateway integrations can populate the runtime argument through automatic configuration without an apps.yaml entry. In those setups the sensor is still published. Describe the condition as configured in apps.yaml or by supported charger auto-discovery.
- predbat.car_charging_power - The current power drawn by your car charger in kW, only published when **car_charging_power** is set in `apps.yaml` (see [car charging](car-charging.md#configure-appsyaml-for-your-car-charging))
- Files reviewed: 48/48 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
The PV, battery and grid arms were drawn at 45 degrees when the angle between the circles they join is 33.7, so each one left its circle off-centre and its arrowhead stopped in open space short of the House. Every arm now runs along the line joining the two centres, which is the geometry the new car arm already used. Aligning them exposed a second problem: the arrowhead is drawn beyond the end of the line, not on it. markerUnits defaults to strokeWidth, so markerWidth 10 on a 2px stroke is 20 user units of arrowhead past the end vertex. With the lines stopping short that overhang was invisible; ending them on the circle edge drove the arrowheads inside the circles they point at. Each line now stops 20 units short and the arrowhead fills the gap, landing its point on the edge. The power readings are placed perpendicular to their arm and symmetric about the House, far enough out that the whole label clears the line rather than being printed across it. Both are covered by geometric tests that failed first: every arm is checked tail-on-one-edge and point-on-the-other, colinear with the two centres, in both flow directions; and every label's text box is checked against every arm for overlap. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Each circle now carries the Material Design Icon of the thing it is - solar panel, battery, transmission tower, home and car - taken from the @mdi/font stylesheet the web interface already loads, drawn in white on a circle coloured for that node: yellow PV, green battery, grey grid, brown house and red car. The name moves into a <title>, so hovering still identifies a circle and the markup still reads as PV/Battery/Grid/House/Car. The battery icon follows the battery: low, medium or high by state of charge, switching to the charging variants of the same three levels when power is flowing in. battery-plus and battery-minus exist but carry no level, so they would trade the state of charge away for a sign the arrow already shows. The PV arm keeps a deeper yellow than its circle. A #FDD835 line is close to invisible against the page, which the circle behind a 44px glyph is not. Two problems found in the existing code while wiring the battery icon up: - battery_charging was inverted. It read battery_power >= 10, but positive is DISCHARGE in Predbat (gateway.py negates the firmware's sign for exactly this reason) and the branch under it draws Battery -> House. Renamed to battery_to_house, with battery_charging now meaning power flowing in. A test asserts the charging icon appears on a negative reading, so it cannot silently invert again. - battery_discharging and grid_exporting were computed and never used, and the first of them was wrong. Removed. Every colour edit is scoped to the diagram: #4CAF50 appears 23 times in web.py but only 12 of those are the diagram, the rest being unrelated buttons and status text. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
apps.yaml validation flagged an unavailable charger. update_car_charging_power deliberately reads a charger sitting at 'unavailable' as zero without recording an error, because that is what an EV charger reports with nothing plugged into it - but validate_config reads the same entity by a separate path and requires its state to parse as a float, so it still ended the run reporting errors. A transient_ok flag on the schema entry lets that one key accept Home Assistant's unavailable/unknown placeholders. Nothing else is loosened: a missing entity is still an error, so a typo in the name is still caught, and a state of 'banana' is still rejected - both covered by tests. The reader now publishes car_charging_power and car_charging_power_configured together, once every sensor has been read. Setting the flag first left a window where the web server, which runs in its own thread and reads the pair independently, could draw a car declared but carrying the previous cycle's power. fetch_inverter_data publishes its accumulated totals in one go for the same reason. Ohme skipped its own power sensor when car_charging_energy was already set to its own energy entity - by hand in apps.yaml, or left behind by an earlier run. Only a third-party energy sensor should hold the power wiring back. The single-item list form is unwrapped before comparing, as that is how the entity round-trips through apps.yaml. The published sensor's documentation said it appears only when the key is set in apps.yaml, but auto-discovery configures it for supported chargers with no apps.yaml entry at all. The unconfigured-publication test no longer deletes the entity by hand before checking it is absent, so it cannot pass on the strength of that deletion, and it now checks all four stores dashboard_item writes to rather than only the HA state. Those stores are cleaned up afterwards so nothing leaks into later tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
car_charging_energy has the same exposure car_charging_power had: an EV charger routinely reports 'unavailable' or 'unknown' with nothing plugged into it, and validate_config required the state to parse as a float, so a car sitting on the driveway left the whole run reporting configuration errors. Nothing downstream needs the reading to be present right now. minute_data() skips unavailable and unknown samples outright (utils.py), so the daily incrementing series is built from the readings that exist, and a charger that really has stopped reporting is already covered by a far better targeted warning in fetch.py - car_charging_hold falling back to the threshold heuristic because no data could be loaded. transient_ok therefore applies to this key too. Validation is not loosened anywhere else: a state of 'banana' is still an error and an entity that does not exist is still an error, so a typo in the name is still caught. Both keys now carry all five cases in the validator's own tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The advice was to wrap a charger reporting 'unavailable' or 'unknown' in a template substituting zero. That is unnecessary and actively harmful. Unnecessary because minute_data() skips unavailable and unknown samples when it loads the history (utils.py), and validate_config no longer reports them as a configuration error, so such a sensor works as it is. Harmful because clean_incrementing_reverse treats any zero as a reset (utils.py:740 - "if nxt <= 0 ..."). On a daily incrementing sensor a zero part way through the day is therefore indistinguishable from the midnight reset, so the series rebases there and the charging recorded before it is counted twice. The utility meter advice above it is unchanged - that addresses a different sensor shape, one reporting cumulative energy per charge rather than per day. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
car_charging_power, an optionalapps.yamlsensor giving the live power (W) drawn by an EV charger, and puts the car on the web interface power flow diagram.It is display only — the plan still models car charging from
car_charging_energyand is unaffected by this.What it does
When
car_charging_poweris set:load_power - car_charging_power(clamped at zero), so it reads as the rest of the household rather than counting the charger twicepredbat.car_charging_power(kW,device_class: power) is published for upstream consumers. It is only published when a charger is configured, so its absence is how a consumer tells "no charger" from "charger idle"When it is not set the diagram renders exactly as it does today and no sensor is published.
Several chargers can be listed and are summed, as
car_charging_energyallows.Automatic configuration
All five components that already wire
car_charging_energynow wire the matching power sensor from the same device list, so entry N is the same charger:car_charging_energycar_charging_powersensor.predbat_ohme_energy_todaysensor.predbat_ohme_power_watts..._session_energy..._power..._evc_energy_active_import_register..._evc_power_active_import..._ev_energy_today..._ev_powersensor.{pfx}_session_energysensor.{pfx}_powerOhme's is tied to the same decision as its energy sensor: when a third-party charger owns the energy figure, Ohme's own power reading is left out rather than reported beside it.
The
apps.yamltemplates ship a regular expression matching Wallbox and Zappi, the same waycar_charging_energydoes. Huawei and Kostal ship charger-specific entries (go-eCharger, evcc) rather than the generic regex, so those get commented placeholders pointing at their own charger.Three cases handled deliberately
Each is covered by a test that failed first:
unavailablewith nothing plugged in (normal for an EV charger) reads as zero without flagging the run as errored —get_arg's numeric path would otherwise leave Predbat sitting in "with Errors" all dayre:template default counts as not configured, so a household with no charger never gets a Car drawn in the window beforeauto_config(final=True)removes the keyauto_config()leaves aNonein place of a list entry whose expression found nothing, and an index walk would stop at the hole instead of the chargers after itTesting
apps/predbat/tests/test_web_power_flow.py(registered asweb_power_flow) — 13 cases covering the reader, unit conversion, the published sensor, all three diagram states and the house-remainder clamptest_ohme.py,test_myenergi.py,test_alphaess_publish.py,test_ge_cloud.pyandtest_gateway.pyDocs
car-charging.md,apps-yaml.md,web-interface.mdandoutput-data.md.🤖 Generated with Claude Code