Skip to content

Repository files navigation

SunCore

Energy management for Home Assistant: peak shaving, PV surplus routing and price-based optimisation for dynamic tariffs — with an Ingress dashboard, native Home Assistant entities, a Lovelace card and Bubble Card modules.

Status: pre-release. Everything described here is implemented and tested, but SunCore has not yet run against real hardware over a long period. It starts in dry run: it plans, shows you exactly what it would do, and writes nothing to your inverter or wallbox until you turn that off deliberately. Please leave it there for a few days before going live.


What it does

SunCore builds a 24-hour plan for your house every five minutes and executes the first step of it.

Peak shaving. Holds grid import below a limit you set, by discharging the battery or throttling loads. The limit is a soft constraint: when it physically cannot be met, the plan degrades and tells you, rather than failing.

PV surplus routing. Sends excess production to the highest-value sink in the order you choose — battery, then thermal storage, then the car. Implemented as a cost ladder inside the optimisation rather than a separate rule engine, so it never fights the price optimisation.

Price optimisation. Buys when electricity is cheap and avoids expensive quarter-hours, using Tibber, aWATTar, EPEX, Nord Pool or ENTSO-E. A fixed tariff is the default, because peak shaving and surplus routing are worth real money without any price spread — a dynamic contract is an addition, not a prerequisite.

Installation

SunCore is two pieces. Install both.

1. The add-on

Settings → Add-ons → ⋮ → Repositories, add:

https://github.com/PXLJAKE/SunCore

Then install SunCore and start it. Open its panel and the setup wizard will look at your Home Assistant and propose everything it finds.

2. The integration

The repository has to be public for HACS. HACS reads hacs.json and manifest.json from raw.githubusercontent.com without authentication, so a private repository fails its checks with messages about invalid files that are actually about visibility — and HACS could not install from it either way.

Install via HACS (Integrations → ⋮ → Custom repositories → this repository, category Integration), then restart Home Assistant.

You should then get a "SunCore discovered" notification — the add-on announces itself, so there is no host or port to type. Confirm it and you get the entities and the Lovelace card.

The card needs no resource entry: the integration serves and registers it, and it appears in the card picker as SunCore.

Setup

The wizard does the work. It shows every entity it found, how confident it is, and why it matched — so you can accept twelve suggestions with one button and still spot the one that is wrong.

Two things are worth your attention:

Check whether your price sensor already includes the fees. This is the setting most worth a minute of your attention, and the wizard asks it outright: "The sensor already includes all fees."

Compare one hour's price in Home Assistant against your supplier's app or your last bill.

What you see What to do
The sensor shows roughly what you actually pay (say 0.28 EUR/kWh) Leave the switch on and leave the surcharge fields empty. SunCore uses the value as it is.
The sensor shows a bare market price (say 0.06 EUR/kWh, sometimes negative) Turn the switch off and fill in your supplier markup, network charges, levies and VAT.

Tibber publishes finished end-customer prices, so the switch defaults to on for it. aWATTar, EPEX, Nord Pool and ENTSO-E publish raw market prices, so it defaults to off — but treat both as guesses and check, because the markup can be configured inside several of those integrations, and a generic price sensor is whatever its author decided.

Getting this wrong is silent in both directions. Miss the surcharges and SunCore optimises against numbers twenty cents away from your bill. Add them on top of a Tibber price that already contains them and every hour looks expensive, so the battery looks permanently worth discharging and your house never charges it.

You can change this later without re-running setup: Settings → Tariff.

Pick your battery. SunCore needs its capacity and its charge and discharge limits before it can plan anything at all. The battery step has a picker covering common storage systems and hybrid inverters, so in most cases you choose two boxes and the numbers arrive filled in.

Pick the inverter as well as the pack. The inverter's AC side is usually what actually limits the house — a 12.8 kWh stack behind a 5 kW inverter does 5 kW — and SunCore takes whichever of the two is smaller. Every figure stays editable, because these are datasheet values and your installer may have configured something else.

Not listed? Choose Enter my own figures, or add it to suncore/backend/hardware_catalog.yaml and open a pull request — it is data, so no code changes and no release. You can also drop your own hardware_catalog.yaml into the add-on's config folder.

Add whatever discovery missed. It recognises the integrations it knows, and that is never everything. At the bottom of the devices step — and later under Settings → Devices — you can add:

Add this For
EV charger Any wallbox with a charging-current number, with or without a start/stop switch
Thermostat / air conditioning Anything in the climate domain, used as thermal storage within a comfort band you set
Water heater / boiler Anything in the water_heater domain
Switchable load Any switch — a pool pump, a dehumidifier, a heating rod
Heat pump Via an SG Ready block switch

Each one asks which entities to use, filtered to the right domain. A device with no control entity mapped is read-only and says so rather than quietly not participating. For a car or a boiler, give it an energy target and a deadline — "8 kWh by 07:00" — and SunCore finds the cheapest way to get there. For a thermostat, give it a comfort band; SunCore moves the setpoint inside it and never outside, which is enforced in the safety layer rather than in the optimiser.

Set your connection rating under Limits. SunCore never plans above it. It is separate from the peak limit: the peak limit is a target you choose, the rating is physics.

Going live

Once you are satisfied with what dry run is showing:

Settings → Devices & services → SunCore → turn off Dry run, or flip switch.suncore_dry_run.

Before you do, test the watchdog. Stop the add-on and confirm every device returns to its safe state. That is the behaviour standing between a modelling error and a battery that charges all night unattended, and it is worth verifying on your own hardware rather than trusting a test suite.

Entities

Entity For
sensor.suncore_status What SunCore is doing; carries the plan as an attribute
sensor.suncore_peak_shaving_limit Current limit
sensor.suncore_grid_target_power Grid setpoint from the active plan
sensor.suncore_current_price Price now; carries the forward curve
sensor.suncore_battery_level / _power Battery state
sensor.suncore_grid_power / _pv_power / _house_power Live flows
sensor.suncore_saved_today Saved since midnight, or unknown if too little data
number.suncore_grid_limit Grid limit — a slider in Bubble Card
select.suncore_mode Automatic / peak shaving / price / off
switch.suncore_peak_shaving / _surplus_routing / _dry_run Toggles
button.suncore_re_plan_now / _stop_and_make_safe Actions
binary_sensor.suncore_grid_limit_exceeded Pop-up trigger, automations
binary_sensor.suncore_setpoint_refused A device ignored a setpoint
binary_sensor.suncore_plan_stale The plan expired; devices are in safe state

Bubble Card

See docs/bubble-card.md for the four modules, ready-made dashboard YAML, and how to embed the card in a pop-up.

Short version: turn on Install Bubble Card modules in the integration's options and SunCore writes them wherever your Bubble Card keeps modules, without ever overwriting something you edited.

Supported hardware

Hardware support is data, not code — each device is a YAML profile describing how to recognise it, what to read, what to write and what its safe state is. Adding hardware is a pull request against suncore/backend/device_profiles/, not a release.

Inverters and storage: Kostal Plenticore, SolarEdge StorEdge, Fronius GEN24, Sungrow SH, Huawei SUN2000/LUNA2000, SMA, Victron ESS, Deye/Sunsynk, SolaX, GoodWe, plus monitoring for BYD, Pylontech and BSLBATT packs.

Wallboxes: evcc, Kostal ENECTOR, OCPP, go-e, Easee, KEBA, Wallbox Pulsar, Tesla.

Thermal: anything in the climate or water_heater domain. Heat pumps are monitored by default and only controlled where a real setpoint entity exists.

Anything else: the generic_* profiles let you map entities by hand, which covers every device Home Assistant can already control. That is the normal path, not a fallback.

Does it actually save anything?

Check, do not take our word for it:

python scripts/backtest.py --database /path/to/suncore.db --days 7

The backtester replays your own recorded history through the optimiser, giving it only the data that existed at each step — no peeking at the rest of the day — and compares against the same house doing what it would do without SunCore: battery on plain self-consumption, deferrable loads starting immediately.

It reports both cost and peak, and says which one actually moved. On a sunny day with a comfortable peak the honest answer is often "almost nothing in money, 1.4 kW off the peak", because ordinary self-consumption already captures most of the money. The tool says so rather than dressing it up.

Try it without any history using --synthetic.

Architecture

┌──────────────────────── Home Assistant ────────────────────────┐
│                                                                │
│  ┌─── Add-on: SunCore ──────────────────┐   ┌── Integration ──┐ │
│  │                                      │   │   (HACS)        │ │
│  │  ha_client ──── websocket ───────────┼───┤  entities       │ │
│  │    │        subscribe_entities       │   │  suncore-card   │ │
│  │    ▼        (deltas, not polling)    │   │  bubble modules │ │
│  │  discovery ──▶ device_profiles/*.yaml│   └────────┬────────┘ │
│  │    │                                 │            │          │
│  │    ▼                                 │  Supervisor discovery │
│  │  adapters ──▶ prices + PV forecast   │◀───────────┘          │
│  │    │                                 │                       │
│  │    ▼                                 │                       │
│  │  optimizer (MILP, 96 × 15 min)       │                       │
│  │    │                                 │                       │
│  │    ▼                                 │                       │
│  │  safety ──▶ actuators ──── REST ─────┼──▶ inverter, wallbox, │
│  │  (clamps, watchdog)   service calls  │    climate, boiler    │
│  │    │                                 │                       │
│  │    ▼                                 │                       │
│  │  FastAPI ──▶ SSE ──▶ Vue 3 dashboard │                       │
│  └──────────────────────────────────────┘                       │
└─────────────────────────────────────────────────────────────────┘

Decisions worth knowing

The optimiser is a MILP, not an LP. Devices that are genuinely on or off, minimum run times and semi-continuous loads — an EV charges at 6 A or not at all — need binary variables. Two things that look like they need them do not: battery charge/discharge exclusivity is already dominated by round-trip losses, and "no grid charging" is expressible as grid import never exceeds consumption, which is linear.

Both grid limits are soft. A hard peak limit plus an empty battery plus non-throttleable base load has no solution, and then there is no plan at all — precisely when one is needed. The connection rating is soft for the same reason: 8 kW of load on a 6 kW connection trips the breaker either way, and SunCore's job is to keep planning and report it.

The solver is resolved at runtime. PuLP 4 publishes no armv7 wheel and replaced its API, so the project is pinned below 4.0; highspy has no armv7 wheel either. So on amd64 and aarch64 SunCore uses HiGHS, and on armv7 it uses the coinor-cbc package the image installs. GET /api/solver reports which one a given install actually got, and the image build fails if there is none.

Safety is separate from optimisation. Hard clamps, rate limits, a setpoint deadband, read-back verification and a watchdog live above the optimiser in safety.py. A modelling mistake must not be able to reach hardware, and an inverter that silently ignores a Modbus write must not look like a success.

Runtime config is not add-on options. /data/options.json belongs to the Supervisor; writing it needs POST /addons/self/options and restarts the add-on. SunCore keeps its configuration in /data/config.json and its history in /data/suncore.db.

Development

# Backend
pip install -e ".[dev]"
uvicorn backend.main:app --reload --port 8099 --app-dir suncore

# Dashboard
npm --prefix suncore/frontend ci
npm --prefix suncore/frontend run gen:api
npm --prefix suncore/frontend run dev

# Lovelace card (output goes into the integration)
npm --prefix lovelace/suncore-card ci
npm --prefix lovelace/suncore-card run build

Every check CI runs, in one command:

bash scripts/check.sh

After changing models.py or main.py, regenerate the contract — CI fails on a stale one, because the frontend's types are generated from it:

python scripts/export_openapi.py
npm --prefix suncore/frontend run gen:api

A .devcontainer/ is provided; it installs CBC so development uses the same solver path as production.

Repository layout

Path Contents
suncore/ The add-on: FastAPI backend, optimiser, Vue dashboard
suncore/backend/models.py Single source of truth for every wire format
suncore/backend/device_profiles/ Declarative hardware support
custom_components/suncore/ HACS integration, built card, Bubble modules
lovelace/suncore-card/ Lit source of the Lovelace card
scripts/backtest.py Replays your history to measure the saving
suncore/openapi.json Generated contract; the dashboard's types come from it

FAQ

Do I need a dynamic tariff? No. A fixed price is the default, and peak shaving and surplus routing work without any price spread.

My Tibber prices already include the fees — what do I fill in? Nothing. Leave the surcharge fields empty and leave "the sensor already includes all fees" on, which is the default for Tibber. Anything you type into those fields would be added on top of a price that already contains it. See Setup.

How often does SunCore re-plan? The full optimisation runs every five minutes over the next 24 hours, and only its first step is executed — so each plan is acted on for at most five minutes before being replaced by one built on newer prices, a newer forecast and the actual state of the battery. Between those, a fast loop runs every two seconds: it reads your meters, updates the dashboard, holds the peak limit and watches for faults. Optimisation does not need to run every second; reacting does.

Do I need a battery? No. Without one, SunCore shifts controllable loads and routes surplus. The dashboard and backtester both handle it.

Will it fight my inverter's own logic? Only where you map a write. Anything not mapped is monitored, not commanded, and the safe state returns every device to its own control.

What happens if the add-on crashes? Every plan carries an expiry. When it lapses — or Home Assistant becomes unreachable, or you press stop — every device returns to its profile's safe state, normally the inverter's own self-consumption mode.

Why is my saving smaller than I expected? Probably because it should be. Run the backtester: if the baseline already captures the value, that is worth knowing rather than being hidden behind an optimistic figure.

It says a setpoint was refused. Your device accepted the write and ignored it, which inverters do when they are in the wrong operating mode. Check binary_sensor.suncore_setpoint_refused and the actuation log on the Devices page.

Safety notice

SunCore controls inverters, battery storage and EV chargers. You remain responsible for complying with your grid operator's requirements and, in Germany, with §14a EnWG. Commissioning starts in dry run; nothing is written to your hardware until you turn that off.

Licence

Apache-2.0

About

Energy management for Home Assistant: peak shaving, PV surplus routing and price-based optimisation for dynamic tariffs, with an Apple-style dashboard and Bubble Card support.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages