Skip to main content

Dynamically adjusts EET SolMate injection profile based on hourly electricity price and weather forecast

Project description

SolMate Optimizer

PyPI version CI Python 3.13+ License

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:

  1. Fetches hourly electricity prices from aWATTar (public API, no auth)
  2. Fetches current weather and forecast from OpenWeatherMap (free API key)
  3. Connects to your SolMate via solmate-sdk (cloud API, serial + password)
  4. Reads the current battery state and existing injection profiles
  5. Computes an optimized 24-hour injection profile based on price quantiles, weather, and time of day
  6. Compares with the current profile — only writes if something changed
  7. 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 30 / 80 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 < 20 % 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 20–75 % + evening medium High price but below high threshold — moderate injection, spread over time
5 Middle prices, night (default 00:00–07:59) night Baseload (fridge, standby); no solar production
5 Middle prices, morning (default 08:00–09:59) + sun expected evening Export PV early at decent prices to keep battery headroom for the midday glut
5 Middle prices, morning (default 08:00–09:59) + no sun low Cloudy morning — let PV charge, preserve battery for the evening
5 Middle prices, daytime (default 10:00–17:59) low Let PV charge the battery
5 Middle prices, evening (default 18:00–23: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 00:00–07:59 (--nighttime / NIGHTTIME, default 24,824 means night begins at midnight). The morning export window defaults to 08:00–09:59 (--morning / MORNING, default 8,10) and is only active on sunny days; the evening start defaults to 18:00 (--evening-start / EVENING_START) and now runs through to midnight.

The morning export band (priority 5) addresses a quirk of the hardware: when the battery is full, the SolMate exports surplus PV to the grid regardless of the profile. On a sunny day the battery would otherwise fill from morning PV and then be forced to dump the strong midday production at the day's lowest (often negative) prices. By exporting the morning production early — at the better morning prices — the battery stays lower into midday and can instead absorb that midday peak. The rule is gated on the sun-expected forecast, so on a cloudy day the morning simply lets PV charge and the battery is kept for the evening.

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 (20 %) 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.20 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 24,8 Nighttime window as start,end (inclusive start, exclusive end, wraps midnight; 24 = night starts at midnight)
MORNING --morning 8,10 Morning export window as start,end (inclusive start, exclusive end); uses the evening level on sunny days, otherwise lets PV charge
EVENING_START --evening-start 18 First evening hour (inclusive). Evening runs from here to nighttime start.
LEVEL_NIGHT --level-night 30,80 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, EVENING_START ≤ nighttime start, and morning start ≤ morning end.

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:

solmate history — 7 days of PV, injection, battery

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%     30     80  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%     30     80  Night/baseload
     5    12.5   100%     30     80  Night/baseload
     6    14.7   100%     30     80  Night/baseload
     7    16.3   100%     30     80  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%     30     80  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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

solmate_optimizer-0.7.0.tar.gz (18.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

solmate_optimizer-0.7.0-py3-none-any.whl (21.9 kB view details)

Uploaded Python 3

File details

Details for the file solmate_optimizer-0.7.0.tar.gz.

File metadata

  • Download URL: solmate_optimizer-0.7.0.tar.gz
  • Upload date:
  • Size: 18.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for solmate_optimizer-0.7.0.tar.gz
Algorithm Hash digest
SHA256 34b2c6f9764537f5df862c1b6a749dec9e1a48863a9d99ddb4f0913a9240fc57
MD5 3c88ba77bb1e02c4028c7b830f6f70c0
BLAKE2b-256 6e8eafd8f4e60188d20e23fe27f6e9cf6a7732246fd7cfa3d3b95861da34ff82

See more details on using hashes here.

Provenance

The following attestation bundles were made for solmate_optimizer-0.7.0.tar.gz:

Publisher: release.yml on haraldschilly/solmate-optimizer

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file solmate_optimizer-0.7.0-py3-none-any.whl.

File metadata

File hashes

Hashes for solmate_optimizer-0.7.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7923f77e20bac7e7eae410f2f04389a272fc3f45b00c29a535483843a207656b
MD5 65f7fbcff699227fd08b52b4f2501864
BLAKE2b-256 f8e86b0b86c785eaa97a8c2b56d75bb72e2711b73608dfa0516e7cc5f2712354

See more details on using hashes here.

Provenance

The following attestation bundles were made for solmate_optimizer-0.7.0-py3-none-any.whl:

Publisher: release.yml on haraldschilly/solmate-optimizer

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page