Fotovoltika: Home Solar Autopilot
Personal project
On sunny weekends the electricity market goes negative, and my rooftop solar would be paying to give its power away. This small system decides a day ahead when to hold export back, switches the inverter on its own, and shows every day in crowns instead of kilowatt-hours: what the house earned, what it paid and what the panels and the battery saved. It runs on a small Raspberry Pi at home and messages my phone if anything stops.

🎯 Why
My house in the Czech Republic has a 10 kW Sofar inverter, 14 panels and a 10.5 kWh battery. I sell surplus power at the spot price, and on sunny weekends that price goes negative for hours: exporting means paying the buyer to take my electricity. The inverter can limit export, but only by hand from its own panel, and it knows nothing about prices. Nobody is going to change that setting twice a day. And even with the right setting, I had no way to tell whether the system was doing well, because the inverter speaks kilowatt-hours and my bills speak crowns.
- Paying to export: negative spot prices turn every exported kilowatt-hour into a fee
- Buying what the roof could make: the inverter's own zero-export mode throttles production below the house's consumption, so the house buys from the grid at the high tariff
- No one at the switch: the export setting is manual, local and easy to forget
- No picture in money: the vendor app shows energy, not what a day earned, cost or saved
- Silent failures: a home automation that stops quietly is worse than none, because you keep trusting it

🤖 The Autopilot
The part that acts. It decides a day ahead when export should be limited, keeps the inverter in that state and gets out of the way when I want to decide myself.
Plans the whole day from spot prices
Tomorrow's 15-minute prices are published the evening before, so the planner builds a schedule for the whole day instead of reacting slot by slot. Every slot whose buy-back price (spot minus the buyer's margin) falls below a small positive threshold is marked, and neighbouring slots form windows. A window that would save less than 1.5 CZK is skipped, because it is not worth two writes to the inverter's memory. A short gap between two windows is merged for the same reason, when the money the gap would earn is smaller than those two writes.

Limits export without throttling the house
During a negative window the inverter switches to limited export with a target of 2 percent of its rating, about 200 W, instead of zero. Zero export turned out to throttle production below the house's consumption: measured at noon, production fell from 3.9 to 0.7 kWh per hour and the house bought 2.5 kWh at the high tariff to avoid a 4.7 CZK export fee. The small positive target gives the regulator room to follow the house, and it is fourteen times cheaper in the same window. Right before limiting, the loop also checks the live flow: if the battery is still taking everything and less than 100 W goes to the grid, it writes nothing.
Heals itself instead of following a timetable
Every five minutes the autopilot compares the inverter's real state with the planned one and fixes the difference. A missed tick, a reboot, a power cut or someone changing the mode on the display all repair themselves. It writes only on change, so the inverter's memory sees hundreds of writes a year instead of tens of thousands. Writes are retried three times, and a process that loses the inverter for three rounds in a row restarts itself.
Lives in the Apple Home app and the menu bar
The controller is a HomeKit bridge with two switches, Export and Autopilot. Flipping Export by hand turns the autopilot off, so the plan never fights a manual decision; turning Autopilot back on hands control back to the plan. On the Mac a menu bar widget shows a green bolt with the current power when exporting, a red stop sign when export is limited, and an orange warning when the data goes stale.
📡 The Data
Everything the page shows and every decision the autopilot makes comes from one SQLite database that a collector keeps filling. A past day can be reopened and audited exactly like today.
- Inverter: a full read of the registers every five minutes, a raw dump of the whole register block every third round for later analysis, and a light 15-second reader for the live power flows
- Spot prices: 15-minute day-ahead prices from the Czech market operator OTE, with energy-charts and a third public source as fallbacks, refreshed every 30 minutes
- Exchange rate: OTE quotes in euros, so every price is converted to crowns with the Czech National Bank's rate for that day
- Weather forecast: Open-Meteo forecasts and reanalysis archive, every model run stored separately, so a forecast can later be compared with what really happened
- Grid: energy-charts data for the Czech grid, including solar output, residual load and CO₂ intensity
- Garden weather station: an Ecowitt station measuring temperature, humidity, solar irradiance, UV, rain, wind and pressure
A weather station that talks to the Pi
The station has no API to ask, but it can upload to a custom server. The Pi listens on the home network and receives a reading about every minute, converting the station's imperial units to metric. When the push is missing, an hourly job backfills the gaps from Ecowitt's cloud history, and a row from the cloud is written only where no local reading exists within two and a half minutes, so the local data always wins. Commands sent to the station go through an allowlist of read-only calls, so a typo can never change its settings.
Collecting the weather for later
Besides prices and the inverter, the system keeps an eye on the weather: what the garden station measures right now and what the forecast says for the coming days. None of it steers the autopilot yet, the plan still runs on prices alone. But every reading and every forecast is kept, so a record keeps growing of what was predicted, what really happened and what the roof made. Using it is next on the plan: once there is enough history, a model of this particular roof can predict tomorrow's production, and the planner can then tell in advance how much would really be exported in a negative window and whether limiting it is worth it.
📊 The Dashboard
A single server-rendered page on the home network that refreshes itself every 20 seconds and works on the phone. The question it answers is always the same: did the system do what it should, and what did that mean in crowns. Any past day is one arrow away.
Now
Four cards: whether export is on, whether the autopilot is in charge and why, the current buy-back price, and the current price with the tariff and how many crowns per hour the house is earning or spending right now. Warnings appear above them when the last control round is stale or the autopilot runs in dry-run mode.
A live scene of the house
The house is traced from a photo into SVG: roof, chimney, 14 panels, door and window in the proportions of the original, the battery on the left and the grid pole on the right. Power flows are animated lines whose speed follows the kilowatts, green when producing, selling or charging, red when buying or discharging, and red again when selling at a negative price. The battery shows charge, health and temperature, the pole the price, the window lights up with the house's load and the door shows indoor climate. Phone and desktop get separate layouts.
- Sky from real data: the station's irradiance is compared with a clear-sky model for the sun's current elevation. The ratio decides between clear, partly cloudy, cloudy and overcast, and how strong the sun's rays are drawn. At dawn, when the model is too small to compare, it carries over the last reliable ratio.
- Night and moon: after sunset the sky switches to stars and the moon in its real phase, computed from the synodic month.
- Rain: drops fall when the station reports rain.
- When the panels were last washed: rain is split into episodes, and an episode counts as a wash only with enough millimetres and enough minutes of heavy rain. A light drizzle does not clean panels.
- The sun today: sunrise, sunset, day length, today's solar dose in kWh/m² and the peak irradiance.

Energy and money
One total on top: what the day earned and saved together, and what is left after paying for the power bought from the grid. Under it four money cards: earned by export, paid for consumption, saved by the panels, saved by the battery. Then four energy cards: produced, used by the house, exported, imported. A sentence says what the day would have cost from the grid alone, how much was exported at a negative price, and for today how long the battery would keep a 150, 250 or 500 W load running in a blackout.

One chart, five layers
The buy-back price per quarter hour as bars, red below the threshold, with a dashed threshold line. Irradiance from the weather station on a fixed 0 to 1000 W/m² scale, so an overcast day looks overcast. The money curve from midnight, solid where measured and dashed where estimated, next to a dotted curve of what the plan would have earned. Under the bars three strips: the plan, what the inverter actually did, and who was in control.

Plan versus reality
The control strip has its own palette, because "not in control" means different failures with different fixes: autopilot in charge, manual override, inverter not answering, not recorded, and nothing running at all, which is drawn as red hatching since it is missing data, not a state. Each planned window shows how much of it was actually kept, and skipped or merged windows are listed with the money involved. The audit ends with one sentence: the plan was kept, or it was not and it cost this much.

Tomorrow
As soon as the market publishes next day's prices, the same chart appears for tomorrow with the planned windows and how many writes to the inverter they will take at most.
🧮 The Calculations
The inverter reports cumulative counters a few times an hour. Prices change every 15 minutes, and the purchase tariff switches between high and low on a distributor schedule. Turning that into honest money takes more than multiplying two totals.
- Spreading counters over price slots: the change of each counter between two readings is spread over the 15-minute slots it covers, and each slot is priced at its own price. A kilowatt-hour exported at noon at a negative price and one at 9 am at a high price never get averaged together.
- Filling gaps by the sun: when readings are missing for more than 20 minutes, the energy in the gap is distributed by weight, preferring the station's measured irradiance, then the weather model's radiation, then pure sun geometry. Every slot remembers which method it used, and the page says so.
- Two tariffs, priced per slot: imports, battery discharge and solar power used directly in the house are each priced at the high or low tariff valid in that quarter hour, from the distributor's published switching schedule. A kilowatt-hour from the battery during the high tariff saves more than one at night.
- Who saved what: solar used directly is production minus export minus what went into the battery. That is what the panels saved. What came back out of the battery is what the battery saved. "Saved" is avoided spending, not income, and the page says it in those words.
- Estimates are labelled: for days before the inverter's battery counters were found, charge and discharge are integrated from power samples and marked as an estimate. The plan's money curve also flags where it had to assume what export would have been.
- Live price per hour: the crowns per hour on the Now card are computed from the live flow and the current slot's price, and hidden below 50 W so noise does not show up as money.
- Why the live numbers never add up: at night the battery may give 510 W while the house takes 330 W and the grid nothing. The rest goes to the inverter itself, which uses 100 to 160 W just to run and loses a little more converting the battery's power for the house.
🛡️ Running Unattended
A home system is only useful if it keeps working while nobody watches it, and says so when it does not.
Tells me when it stops
A watchdog, deliberately independent of everything it checks, runs every five minutes. It checks the services, the age of the last control round, the last inverter record and the last weather-station push, whether spot prices cover the next hour, and the disk. A new problem goes to my phone through a Telegram bot at once, a lasting one is repeated every twelve hours, and a fixed one gets an all-clear.
Backs itself up
Every night the Mac pulls a consistent snapshot of the database from the Pi, made through SQLite's backup API so a write in progress cannot corrupt it, compressed from 117 MB to 24 MB. The last fourteen are kept.
Can be fixed from anywhere
Away from home I can open the dashboard, connect over SSH or start a Claude Code session that runs on the Pi itself, reads its logs and fixes things in place. That session shares its project memory with the one on my Mac through the repository, so it knows the same tariffs, decisions and lessons.
🧰 Technology And Why
Built entirely with Claude Code. The rule for every choice was: the fewest moving parts that a single person can keep running for years.
- Python, almost no dependencies: three libraries in total, for the inverter tunnel, HomeKit and reading invoices. Everything else is the standard library, so an upgrade cannot break what is not there.
- pysolarmanv5 over the existing Wi-Fi logger: the Solarman LSW-3 wraps Modbus in its own tunnel. Using it meant no extra hardware next to the inverter. The register map is undocumented, so every register was verified by measurement: battery voltage times current matched reported power within 0.2 percent.
- HAP-python as a HomeKit bridge: the phone already has a good switch UI and Siri. No app to build, nothing to install.
- SQLite: one file, no server, easy to back up and to query when something looks wrong.
- Server-rendered HTML and hand-drawn SVG: no JavaScript framework, no build step. The page loads instantly on the phone, and the scene and charts are exactly what I drew, not what a chart library allows.
- Public APIs for prices, rates, weather and grid: OTE, the Czech National Bank, Open-Meteo and energy-charts are free, stable and need no account. Each source has a fallback or a cache, so one outage does not stop the plan.
- Local push from the weather station: faster and more private than the cloud, with the cloud as backfill only.
- Raspberry Pi 5 with an NVMe disk and systemd: a small computer that does nothing else, starts every service at boot, waits for the clock to sync (the Pi has no battery-backed clock, and a plan for the wrong day is worse than no plan) and restarts anything that crashes. A laptop that sleeps cannot do this.
- Telegram bot for alerts: a push notification on the phone with one HTTP call, no account to run, no server to maintain.
- Tailscale: the Pi, the Mac and the phone in one private network without opening anything on the router.
- Git as the shared brain: code goes through a private GitHub repository in both directions, and Claude's project memory lives in the same repository. Configuration, secrets and live data never enter git.
📐 Principles
Rules that every part of the system follows, so a change in one place does not quietly break the trust in another.
- Safe by default: when prices are missing, outside 04:00 to 22:00, or when a file named DISABLED exists, the target is export allowed. The night rule ignores prices and the plan on purpose, so a bug, bad data or a stuck state can never leave export limited into the next sunny day: whatever goes wrong, the inverter is back in a safe state by 22:00. At night the panels produce nothing, so the rule costs nothing, and like any other target it is written only on change, so an ordinary night costs no write at all
- Measured, not trusted: every number on the page has a source and a stated confidence: measured, integrated or estimated
- Gaps stay gaps: missing data is drawn as missing, never bridged
- One meaning per colour: control states have their own palette, so red never means two things
- Alive is not the same as working: a process that stops answering the inverter is killed and restarted, because a service manager restarts a crash but not a process that is alive and useless
- Written down: the README records what was measured, when and what it disproved, so the next change does not repeat the mistake
🌊 Impact
The inverter runs on autopilot without anyone touching it. Negative-price hours no longer cost money, the house no longer buys power it could have made itself, and every day ends as four numbers I actually care about: earned, paid, saved by the panels, saved by the battery. It lives on a Raspberry Pi, messages my phone when something stops, backs itself up every night and can be fixed from anywhere.