Dynamically adjusts EET SolMate injection profile based on hourly electricity price and weather forecast
Project description
SolMate Optimizer
Dynamically adjusts EET SolMate solar battery injection profiles based on real-time electricity prices and weather data.
Run as a one-shot script once per hour — locally via cron, on GCP Cloud Run, or any other scheduler.
Data sources
Electricity prices: aWATTar
aWATTar Austria provides a free public API with hourly day-ahead electricity prices for the Austrian market (EPEX spot). No API key or registration needed.
- Endpoint:
GET https://api.awattar.at/v1/marketdata - Returns prices in EUR/MWh for the next ~24 hours
- Currently only the Austrian market is supported. Other hourly price providers (Tibber, ENTSO-E, etc.) could be added in the future.
Weather: OpenWeatherMap
OpenWeatherMap provides current weather and a 5-day/3-hour forecast. Used to determine cloud coverage (current and forecast).
- You need a free API key — sign up at openweathermap.org/api, the free tier is sufficient (current weather + 5-day forecast)
- The forecast is used to decide whether the battery can recharge via solar (sun expected vs. persistent overcast)
SolMate: solmate-sdk
The solmate-sdk connects to your EET SolMate via WebSocket (cloud API). Used to read battery state, read/write injection profiles, and activate the optimized profile.
What it does
Every run:
- Fetches hourly electricity prices from aWATTar (public API, no auth)
- Fetches current weather and forecast from OpenWeatherMap (free API key)
- Connects to your SolMate via solmate-sdk (cloud API, serial + password)
- Reads the current battery state and existing injection profiles
- Computes an optimized 24-hour injection profile based on price quantiles, weather, and time of day
- Compares with the current profile — only writes if something changed
- Writes and activates the profile on the SolMate (existing profiles are preserved)
Decision logic
The optimizer is price-driven — electricity prices already encode weather, demand, and time-of-day patterns.
Injection levels
Named power levels (all configurable via env / CLI):
| Level | Default | Used for |
|---|---|---|
| zero | 0 / 0 W | No injection (negative or cheap prices) |
| night | 20 / 50 W | Nighttime baseload |
| low | 0 / 50 W | Battery protection, daytime PV charging |
| evening | 50 / 120 W | Household evening consumption |
| medium | 100 / 200 W | High price, no sun / evening moderate battery |
| high | 200 / 400 W | High price, sun expected |
Priority table
| Priority | Condition | Level | Reasoning |
|---|---|---|---|
| 1 | Price < 0 (negative) | zero | Grid pays consumers to take power — never inject |
| 2 | Price below P25 of 24 h | zero | Electricity is cheap, save battery for when it matters |
| 3 | Battery < 25 % | low | Protect battery regardless of price |
| 4 | Price above P75 + battery OK + sun expected + not nighttime | high | Inject hard when it pays off and battery can recharge |
| 4 | Price above P75 + battery OK + no sun + not nighttime | medium | Price is high but can't recharge — be cautious |
| 4 | Price above P75 + battery 25–75 % + evening | medium | High price but below high threshold — moderate injection, spread over time |
| 5 | Middle prices, night (default 23:00–07:59) | night | Baseload (fridge, standby); no solar production |
| 5 | Middle prices, daytime (default 08:00–17:59) | low | Let PV charge the battery |
| 5 | Middle prices, evening (default 18:00–22:59) | evening | Cover active household consumption |
Priority 4 is intentionally skipped during nighttime: there is no solar production overnight, so injecting aggressively would drain the battery before the sun rises. The nighttime window defaults to 23:00–07:59 and is configurable via --nighttime / NIGHTTIME. The evening start defaults to 18:00 and is configurable via --evening-start / EVENING_START.
During evening hours priority 4 distinguishes two battery bands. If the battery is at or above BATTERY_HIGH_THRESHOLD (default 75 %), the full power level applies. If the battery is between BATTERY_LOW_THRESHOLD (25 %) and BATTERY_HIGH_THRESHOLD (75 %), the medium level is used — the high price still warrants more than pure baseload, but without solar recharging available it makes sense not to drain the battery too aggressively.
Price-based rules (priorities 1 and 2) always win over battery protection: even a low battery should not inject when prices are negative or very cheap.
For the reasoning behind these rules — why relative prices, why the sun-expected heuristic works the way it does, what the optimizer deliberately does not do — see DESIGN.md.
Install
Requires Python 3.13+.
Option A — run without installing (recommended for quick use)
With uv installed, run the latest version directly — no clone, no virtualenv:
uvx --from solmate-optimizer@latest solmate # run optimizer
uvx --from solmate-optimizer@latest status # read-only status view
The --from flag is required because the package name (solmate-optimizer) differs from the executable names (solmate, status). uvx fetches the package from PyPI into an ephemeral environment on first run and caches it for later invocations. Pin to a specific version with e.g. solmate-optimizer@0.2.0.
Option B — install via pip
pip install solmate-optimizer
solmate # run optimizer
status # read-only status view
Option C — from source (for development)
git clone https://github.com/haraldschilly/solmate-optimizer.git
cd solmate-optimizer
uv sync
uv run solmate
Configuration
All configuration is via environment variables and/or CLI options. CLI options override environment variables.
| Env variable | CLI option | Default | Description |
|---|---|---|---|
SOLMATE_SERIAL |
--serial |
— | Your SolMate's serial number (required) |
SOLMATE_PASSWORD |
--password |
— | Your SolMate's user password (required) |
OWM_API_KEY |
--owm-api-key |
— | OpenWeatherMap API key (free tier works) |
LOCATION_LATLON |
--location |
48.2:16.32 |
Latitude and longitude as lat:lon (default: Vienna) |
TIMEZONE |
--timezone |
Europe/Vienna |
Timezone for price/weather hour matching and display (use IANA names, e.g. Europe/Berlin) |
SOLMATE_PROFILE_NAME |
--profile-name |
dynamic |
Name of the injection profile to create/update |
BATTERY_LOW_THRESHOLD |
--battery-low |
0.25 |
Battery fraction (0–1) below which injection is throttled |
BATTERY_HIGH_THRESHOLD |
--battery-high |
0.75 |
Battery fraction (0–1) required for high-price injection during evening hours |
CLOUD_SUN_THRESHOLD |
--cloud-sun-threshold |
60 |
Forecast cloud % below which "sun expected" for recharging |
MAX_WATTS |
--max-watts |
800 |
SolMate max injection capacity in watts |
NIGHTTIME |
--nighttime |
23,8 |
Nighttime window as start,end (inclusive start, exclusive end, wraps midnight) |
EVENING_START |
--evening-start |
18 |
First evening hour (inclusive). Evening runs from here to nighttime start. |
LEVEL_NIGHT |
--level-night |
20,50 |
Night/baseload injection level as min,max watts |
LEVEL_LOW |
--level-low |
0,50 |
Low injection level as min,max watts (battery protection, daytime) |
LEVEL_EVENING |
--level-evening |
50,120 |
Evening consumption injection level as min,max watts |
LEVEL_MEDIUM |
--level-medium |
100,200 |
Medium injection level as min,max watts (high price, no sun) |
LEVEL_HIGH |
--level-high |
200,400 |
High injection level as min,max watts (high price, sun expected) |
Level values are validated: min must be ≤ max, min ≥ 0, max ≤ MAX_WATTS, and EVENING_START ≤ nighttime start.
Run
Set the required credentials in the environment, then invoke either the installed entry point (Option A/B above) or uv run when working from source (Option C):
export SOLMATE_SERIAL="your-serial"
export SOLMATE_PASSWORD="your-password"
export OWM_API_KEY="your-owm-key"
solmate # run optimizer (default)
solmate optimize --dry-run # compute profile, don't write
solmate optimize --no-activate # write but don't activate
status # read-only status view
status --graph # status with plotext profile graphs
history # plot last 7 days of PV/injection/battery
When using uvx, prefix every command with uvx --from solmate-optimizer@latest (e.g. uvx --from solmate-optimizer@latest solmate optimize --dry-run). When working from a checkout, prefix with uv run.
Commands
| Command | Description |
|---|---|
solmate |
Run the optimizer (default, no subcommand needed) |
solmate optimize |
Explicit optimizer subcommand |
solmate optimize --dry-run |
Compute and display profile, but don't write or activate it |
solmate optimize --no-activate |
Write the profile to SolMate, but don't activate it |
status |
Show live values and injection profiles (read-only, no OWM/aWATTar needed) |
status --graph |
Same, with ASCII art visualization of each profile |
status --max-watts 600 |
Override max watts for display (also via MAX_WATTS env) |
history |
Plot the last 7 days of PV, injection and battery with a dual y-axis (watts left, battery % right). Fills the terminal width and ~2/3 of its height (min 30 lines) |
history --days 2 |
Use a different time window |
history --raw |
Dump the full JSON response (with all numeric arrays) to stdout instead of plotting |
history --no-plot |
Print the response structure summary instead of plotting |
history --dump logs.json |
Also write the full JSON response to a file (plot is still shown) |
history --from-file logs.json |
Re-plot a previously dumped response without hitting the cloud |
history screenshot
solmate history renders the last 7 days of PV production, grid injection and battery state on a dual-axis ASCII chart that fills the terminal:
Example output
======================================================================
SolMate Optimizer — 2026-04-13 18:21 CEST
======================================================================
aWATTar: 24 hourly prices loaded
OpenWeatherMap: clouds 0%, 24h forecast
SolMate: PV=16W, inject=95W, battery=97%
Price now: 15.1 ct/kWh (P25=12.0, P75=14.7, range: 11.5 – 16.5 ct/kWh)
Battery: 97%
Clouds now: 0%
Hourly profile 'dynamic':
Hour ct/kWh Cloud MinW MaxW Reason
---- ------ ----- ----- ----- ----------------------------------------
0 12.0 97% 20 50 Night/baseload
1 11.9 97% 0 0 Price low (11.9 ct <= P25=12.0 ct)
2 12.0 97% 0 0 Price low (12.0 ct <= P25=12.0 ct)
3 11.8 98% 0 0 Price low (11.8 ct <= P25=12.0 ct)
4 12.0 99% 20 50 Night/baseload
5 12.5 100% 20 50 Night/baseload
6 14.7 100% 20 50 Night/baseload
7 16.3 100% 20 50 Night/baseload
8 16.0 100% 100 200 Price high (16.0 ct >= P75=14.7 ct), no sun expected
9 14.5 100% 0 50 Daytime, let PV charge
10 12.6 99% 0 50 Daytime, let PV charge
11 12.9 99% 0 50 Daytime, let PV charge
12 11.9 98% 0 0 Price low (11.9 ct <= P25=12.0 ct)
13 11.5 96% 0 0 Price low (11.5 ct <= P25=12.0 ct)
14 11.9 95% 0 0 Price low (11.9 ct <= P25=12.0 ct)
15 12.6 97% 0 50 Daytime, let PV charge
16 13.5 98% 0 50 Daytime, let PV charge
17 14.8 100% 100 200 Price high (14.8 ct >= P75=14.7 ct), no sun expected
* 18 15.1 78% 100 200 Price high (15.1 ct >= P75=14.7 ct), no sun expected
19 16.5 55% 100 200 Price high (16.5 ct >= P75=14.7 ct), no sun expected
20 15.8 33% 100 200 Price high (15.8 ct >= P75=14.7 ct), no sun expected
21 13.8 43% 50 120 Evening consumption
22 13.3 53% 50 120 Evening consumption
23 12.3 63% 20 50 Night/baseload
Profile 'dynamic'
┌────────────────────────────────────────────────────────┬────────────────┐
400┤ │ │
│ │ │
200┤ ▖ ▗▄▄▄▄▄▄▄▄▄▖ │
│ ▞▝▖ ▗▘ │ ▝▚▖ │
│ ▞▄▚▝▚ ▗▘▞▀▀▀▀▀▀▀▀▀▚▄▝▀▀▀▀▄▖ │
0┤▚▄▄▄▄▄▄▄▄▄▄▄▞▀▀▀▀▀▀▀▀▀▀▘ ▝▚▀▀▀▀▀▀▀▄▄▄▄▄▄▄▄▄▄▄▄▞▀▀▀▘▘ │ ▀▀▀▀▀▀▝▀│
└┬────────┬─────────┬────────┬─────────┬────────┬────────┴─────────┬──────┘
0 3 6 9 12 15 18 21
How injection profiles work
The SolMate stores named injection profiles, each containing two 24-element arrays:
min[24]— minimum injection per hour (fraction 0.0–1.0 of 800W max)max[24]— maximum injection per hour
Index 0 = midnight, index 23 = 11 PM. The optimizer creates/updates a profile (name configurable via SOLMATE_PROFILE_NAME, default "dynamic") and activates it, leaving your existing profiles ("Sonnig", "Schlechtwetter", etc.) untouched. You can switch back to any profile via the EET app at any time.
Deployment
See DEPLOYMENT.md for instructions on running this on GCP Cloud Run with Cloud Scheduler (hourly cron).
Dependencies
- solmate-sdk — EET SolMate WebSocket API client
- httpx — HTTP client for aWATTar and OpenWeatherMap
License
Apache 2.0 — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file solmate_optimizer-0.6.0.tar.gz.
File metadata
- Download URL: solmate_optimizer-0.6.0.tar.gz
- Upload date:
- Size: 17.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
90e6ebf127d8e40420445d3a2ee11ca73577aeb5eb8f477ac91d1ad39b2ee770
|
|
| MD5 |
a25d8b68f969746175d8650eaeb82a93
|
|
| BLAKE2b-256 |
8f6570b656896f30e14052e8dd4ef35e0fb0efb122f634d0dcb6b4409da6f205
|
Provenance
The following attestation bundles were made for solmate_optimizer-0.6.0.tar.gz:
Publisher:
release.yml on haraldschilly/solmate-optimizer
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
solmate_optimizer-0.6.0.tar.gz -
Subject digest:
90e6ebf127d8e40420445d3a2ee11ca73577aeb5eb8f477ac91d1ad39b2ee770 - Sigstore transparency entry: 1349268942
- Sigstore integration time:
-
Permalink:
haraldschilly/solmate-optimizer@99d029067ee68ec8f78d2717d468205d4bef1a30 -
Branch / Tag:
refs/tags/v0.6.0 - Owner: https://github.com/haraldschilly
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@99d029067ee68ec8f78d2717d468205d4bef1a30 -
Trigger Event:
push
-
Statement type:
File details
Details for the file solmate_optimizer-0.6.0-py3-none-any.whl.
File metadata
- Download URL: solmate_optimizer-0.6.0-py3-none-any.whl
- Upload date:
- Size: 20.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d00b617b4855eaca325ad149ae5e3e97a6bea014349246ee62a4bb4935bc3b7d
|
|
| MD5 |
7943cadf0fa42551cc85ae1f22f18a5c
|
|
| BLAKE2b-256 |
77745030600ef897be3ed3babb8cbb52fc014074a89ddd19a7389705897acf43
|
Provenance
The following attestation bundles were made for solmate_optimizer-0.6.0-py3-none-any.whl:
Publisher:
release.yml on haraldschilly/solmate-optimizer
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
solmate_optimizer-0.6.0-py3-none-any.whl -
Subject digest:
d00b617b4855eaca325ad149ae5e3e97a6bea014349246ee62a4bb4935bc3b7d - Sigstore transparency entry: 1349269025
- Sigstore integration time:
-
Permalink:
haraldschilly/solmate-optimizer@99d029067ee68ec8f78d2717d468205d4bef1a30 -
Branch / Tag:
refs/tags/v0.6.0 - Owner: https://github.com/haraldschilly
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@99d029067ee68ec8f78d2717d468205d4bef1a30 -
Trigger Event:
push
-
Statement type: