Synaplan
开源 AI 软件平台,面向企业:自托管 AI 代理,符合 GDPR 的聊天机器人小部件,RAG 知识库,WhatsApp 与 Outlook AI,MCP,多模型路由。支持 Docker、Kubernetes 或在 Germ 托管。
AI 翻译
尚未收集仓库信息。
尚未检测到更新。GitHub 发布版本后会显示在这里。
Plans boilers and EV chargers into the cheapest 15-minute day-ahead slots using real Energy-Charts prices; Telegram plan, Shelly plugs.
翻译准备中
取自仓库 README
正在加载完整 README。
查看完整 README ↗▶ Live demo in your browser · Case study · Part of Daniil Dushin's portfolio
Plans when a hotel or café runs its flexible electrical loads (water boiler, EV charger, laundry dryer, dishwasher pre-heat, pool pump) using the real day-ahead electricity prices of its bidding zone. Every afternoon it sends tomorrow's plan to Telegram with the expected saving, shows it on a web page, and can switch Shelly smart plugs (dry run by default).
Under Article 11 of Directive (EU) 2019/944, EU countries must let any customer with a smart meter ask for a dynamic electricity price contract, from at least one supplier and from every supplier with more than 200 000 customers. On such a contract the energy part of the bill follows the day-ahead market, where the price of the same kWh can differ several times over within one day. Since 1 October 2025 that market has prices for every 15 minutes.
A boiler with a big tank, a dryer, an EV that is parked overnight: these loads do not care when they run, as long as it happens inside a window. Moving them to the cheap hours saves money with no new equipment, but nobody at a hotel reception will read a price curve every afternoon. This program does it and writes the result in plain words.
devices.yaml: power, run time per day, allowed window (e.g. 22:00-07:00), latest
finish, one block or split with a minimum block length, optional Shelly plug, and a power
cap for all these loads together.Decimal, never float.02:15 CEST–02:00 CET).GET /api/plan (JSON) and GET /chart.svg./today and /tomorrow on
demand. Without a token the message is printed in the console.python -m app backtest --days 30 replays real past days.The 25-hour day when the clocks went back (25 October 2026, real prices):
| Daily plan in Telegram |
|---|
| [screenshot placeholder] |
flowchart LR
EC[Energy-Charts API<br/>or ENTSO-E] -->|missing days only| S[PlanService]
F[bundled real prices<br/>demo mode] --> C
S <--> C[(SQLite<br/>price cache)]
Y[devices.yaml] --> P
S -->|15-minute grid| P[planner.py<br/>pure functions]
P --> W[FastAPI page<br/>SVG chart, JSON]
P --> T[Telegram<br/>daily plan]
P --> SH[Shelly plugs<br/>dry run by default]
P --> B[backtest]
22:00-07:00 means Saturday 22:00 to Sunday 07:00. That way
tonight's run is planned together with tomorrow's prices: the auction closes at 12:00 CET
and its results come out shortly before 13:00, long before the evening. Windows are converted to UTC with zoneinfo, so the same window is 8 hours
long on the spring DST night and 10 hours long in autumn.contiguous: true): sliding window with prefix sums, O(n);min_block: a small dynamic program over (slot, slots taken, length of the
current block). A test checks it against brute force on 300 random cases.Decimal (json.loads(..., parse_float=Decimal)); power is Decimal kW. Costs are exact
and rounded to cents only for display.uv run python -m app backtest --days 30
Real DE-LU 15-minute prices from 26 September to 25 October 2026, with the sample devices in devices.yaml (90 kWh of flexible load per day, power cap 18 kW):
| Device | kWh/day | Baseline €/day | Planned €/day | Saving €/day | Saving |
|---|---|---|---|---|---|
| Hot-water boiler, 6 kW × 3 h | 18 | 2.91 | 2.45 | 0.47 | 16.0 % |
| EV charger, 11 kW × 4 h | 44 | 9.51 | 6.09 | 3.42 | 36.0 % |
| Dishwasher pre-heat, 9 kW × 1 h | 9 | 1.24 | 1.23 | 0.01 | 0.8 % |
| Laundry dryer, 5 kW × 2 h | 10 | 1.58 | 0.87 | 0.72 | 45.2 % |
| Pool pump, 1.5 kW × 6 h | 9 | 1.22 | 0.95 | 0.27 | 22.3 % |
| All devices | 90 | 16.47 | 11.59 | 4.88 | 29.6 % |
Over the 30 days: 347.55 € instead of 494.03 €, a saving of 146.47 €. A test pins these numbers, so they cannot drift away from the code.
How to read it honestly:
Requires uv (it installs Python 3.12 itself).
cp .env.example .env
uv sync
uv run python -m app
Open http://127.0.0.1:8109. Demo mode is on in .env.example (or pass --demo): the
prices are the bundled real file, the clock is stopped at the moment it was downloaded
(24 October 2026, 17:27 in Berlin), so "tomorrow" is Sunday 25 October, the 25-hour day.
The Telegram message and the dry-run plug commands are printed in the terminal. Use the
arrows to go back through 36 days.
Other commands: uv run python -m app plan --day 2026-10-22 prints one plan,
uv run python -m app backtest --days 30 replays history. With Docker:
cp .env.example .env && docker compose up --build.
.env set DEMO_MODE=false, your BIDDING_ZONE and TIMEZONE./id there, and set BOT_TOKEN and PLAN_CHAT_ID.uv run python -m app on any always-on machine in the building (a mini PC or a Raspberry
Pi is enough). The plan for tomorrow goes out at SEND_AT (14:00 by default).shelly: to a device and watch the log for a few days. Only then set
SHELLY_DRY_RUN=false. A plug carries about 2.5–3.5 kW; a 6 kW boiler needs a Shelly
relay driving a contactor, installed by an electrician.uv run python -m app fetch --start 2026-09-20 --end 2026-10-25 fills the cache in one
request (add --save-fixture file.json to keep the raw answer, which is how the demo file
was made).
See .env.example.
| Variable | Default | Meaning |
|---|---|---|
DEMO_MODE | true in the example | Bundled real prices, offline, fixed clock |
DEVICES_FILE | devices.yaml | Devices and power cap |
BIDDING_ZONE | DE-LU | Zone as Energy-Charts names it |
TIMEZONE | Europe/Berlin | Local time for windows, plans and messages |
PRICE_SOURCE | energy-charts | Or entsoe (needs ENTSOE_TOKEN) |
ENERGY_CHARTS_URL | https://api.energy-charts.info | API base URL |
ENTSOE_TOKEN | empty | ENTSO-E Transparency Platform API token |
BOT_TOKEN / PLAN_CHAT_ID | empty | Telegram bot and chat; empty = console |
SEND_AT / SEND_RETRY_UNTIL | 14:00 / 20:00 | When to send tomorrow's plan, and until when to wait for late prices |
SHELLY_DRY_RUN | true | false really switches plugs |
DATABASE_PATH | data/energy.db | SQLite price cache |
HOST / PORT | 127.0.0.1 / 8109 | Web page |
GET https://api.energy-charts.info/price?bzn=DE-LU&start=2026-09-20&end=2026-10-25,
downloaded on 24 October 2026 at 15:27 UTC, with that metadata in the file. The small
files in tests/fixtures/ are real answers for DST days and for the switch from hourly to
15-minute prices, downloaded the same way.planner.py has no I/O, so the DST, midnight and power-cap cases are
plain unit tests with real price files./price endpoint allows two
requests per minute. The service asks for whole days in one request, remembers failures
for 10 minutes (tomorrow's prices before the auction) and backs off on 429.app/
prices.py 15-minute grid, resolution detection, local days
energy_charts.py Energy-Charts client and parser (Decimal, 429, licence)
entsoe.py optional ENTSO-E client and XML parser
devices.py devices.yaml: durations, windows, latest finish, Shelly
planner.py slot choice, power cap, baseline, costs (pure)
service.py cache first, API for what is missing, plans
db.py SQLite: prices, licence, sent plans
backtest.py replay past days
chart.py inline SVG chart
messages.py Telegram text, money and time formatting
scheduler.py daily message, plug switching
shelly.py Shelly Gen1/Gen2 commands, dry run
bot.py /today, /tomorrow
web.py FastAPI routes; templates/ has the page
main.py command line: serve, plan, backtest, fetch
devices.yaml sample devices (edit for your site)
fixtures/ bundled real prices for the demo and the backtest
tests/ pytest suite, tests/fixtures/ has real DST-day prices
uv run pytest
uv run ruff check . && uv run ruff format --check .
108 tests: DST days (23 and 25 hours, hourly and 15-minute, real data), the switch from hourly
to 15-minute prices, windows across midnight, one block vs split, the min-block dynamic
program against brute force, the power cap, baseline costs, API parsing and rate limits with
httpx.MockTransport, ENTSO-E XML, the Telegram message, the daily send and retry, Shelly
URLs and dry run, the web page, and the backtest numbers above.
See BUGFIX.md.