With the balcony PV system I got two numbers out of the house: what the panels produce and what flows through the grid meter. Next I wanted them in one place, stored, and as nice graphs.
The setup
Three containers and one service on my home server:
- openHAB talks to all devices and runs the rules.
- Mosquitto is the MQTT broker, mainly for the AhoyDTU.
- InfluxDB 2 stores every value. It runs directly on the host, not in a container.
- Grafana draws the graphs.

openHAB
I keep the whole openHAB configuration in text files and not in the UI. That way I can diff it, review it and put it into git.
It was not always like that. Like most people, I first clicked the MQTT broker and the Shellys together in the UI. Then they only exist in openHAB’s database: not in git, no history, and gone when the server has to be set up again. So I moved them into files, the steps are at the end of this section.
The Shellys have their own binding. A thing per device, addressed by its host name, so a new IP from the router does not break anything:
Thing shelly:shellyem3:em3 "Shelly EM3" [
deviceIp="shellyem3-xxxxxxxxxxxx.lan",
updateInterval=10
]
Thing shelly:shellypro1pm:boiler "Boiler" [
deviceIp="shellypro1pm-xxxxxxxxxxxx.lan",
updateInterval=60
]
The inverters come in via MQTT, as shown with the balcony PV system. Everything ends up as an item with a unit:
Number:Power DeviceAccumulatedWatts "Grid power (net)" (gPersistence) { channel="shelly:shellyem3:em3:device#accumulatedPower" }
Number:Power BoilerPowerConsumption "Boiler power" (gPersistence) { channel="shelly:shellypro1pm:boiler:meter#currentPower" }
Switch RelayOutput "Boiler" { channel="shelly:shellypro1pm:boiler:relay#output" }
Number:Power HM400_ch0_P_AC "Inverter SW power" (gPersistence) { channel="mqtt:topic:OMV-Broker:ahoy:balkon_sw_P_AC" }
Moving things from the UI into files
Back to the broker and the Shellys from the UI. openHAB keeps everything created in the UI in its own database (userdata/jsondb). The files in conf/ are a second source. A thing can only come from one of them, and two things with the same ID clash. So the order matters:
- Copy its ID, the UID (for example
mqtt:broker:OMV-Broker), and write the file with exactly this UID. The items are linked to the UID, so they keep working. - Delete the thing in the UI.
- Only then save the file, or change it once more. openHAB loads a file only when its content changes. Touching it is not enough.
- Check that the thing is ONLINE. My MQTT things were stuck in
HANDLER_MISSING_ERRORuntil I restarted openHAB.
Items behave differently. If an item exists in the UI and in a file, the file wins. But the UI copy stays in the database, and the UI does not let you delete it anymore. The API Explorer in the developer tools can, with DELETE /items/{name}.
InfluxDB
openHAB writes every update of these items into the InfluxDB bucket openhab_db. That is the raw data. The grid power, for example, arrives every 10 seconds. The connection is services/influxdb.cfg:
url = https://influxdb.example.org version = V2 token = ... # org db = home # bucket retentionPolicy = openhab_db
Yes, the bucket goes into retentionPolicy and the org into db. openHAB uses the parameter names from InfluxDB 1 for version 2 as well.
What gets stored is in persistence/influxdb.persist:
Strategies {
everyMinute : "0 * * * * ?"
}
Items {
*: strategy = everyUpdate
gPersistence*: strategy = everyMinute
MarketNet, TotalNet : strategy = forecast
}
Every item on every update, and the power items in gPersistence also once a minute, so a graph has no gaps when the value does not change. The prices use forecast, which also stores the values of the coming hours.
One trap when I moved the token into the file: openHAB stores UI settings in userdata/config/*.config, and there a = in the token is written as \=. I copied it as it was, and for 28 minutes nothing was stored. After any change here, check that new points actually arrive.
But raw data is not what I want to look at. “How much did I import yesterday?” means integrating a power curve, and doing that in every Grafana panel is slow and easy to get wrong. So a few Flux tasks inside InfluxDB calculate the derived values and write them into a second bucket, openhab_db_calc:
| Task | Writes | Runs |
|---|---|---|
| Grid energy hourly | import and export kWh per hour | every 5 min |
| PV energy hourly | PV kWh per hour, from the inverters’ daily counters | every 5 min |
| Energy 15m | grid import/export and PV kWh per quarter hour | every 5 min |
| PV yield daily | kWh per inverter and day | every few min |
| Energy balance daily | import, export, PV, self-used PV, consumption per day | every 15 min |
| Grid power 15-min average | the quarter-hour average, as the smart meter sees it | every minute |
| Energy money hourly | cost of import, value of export, PV savings | every 5 min |
Two rules made this reliable:
- Every task recalculates yesterday and today. Late or missing values get fixed with the next run.
- Every value is stamped with the start of its interval. And the second bucket can be deleted and rebuilt from the raw data at any time.
An example, the hourly PV energy. The inverters report a “yield today” counter that starts at zero every morning, so the energy per hour is simply the increase of that counter:
from(bucket: "openhab_db")
|> range(start: date.sub(d: 1d, from: today), stop: now())
|> filter(fn: (r) => r._measurement == "HM300_ch0_YieldToday" or r._measurement == "HM400_ch0_YieldToday")
|> aggregateWindow(every: 1h, fn: max, createEmpty: false, timeSrc: "_start")
|> window(every: 1d)
|> duplicate(column: "_value", as: "total")
|> difference(keepFirst: true)
// first hour of the day, or a counter reset: count the hour in full
|> map(fn: (r) => ({r with _value: if not exists r._value or r._value < 0.0 then r.total else r._value}))
|> window(every: inf)
|> group(columns: ["_time"])
|> sum() // both inverters
// ... rename to PvEnergyHourly_kWh and write to openhab_db_calc
Nice side effect of the counter: a gap in the readings only moves energy into the next hour. Nothing gets lost, and the hours always add up to the daily yield.
Time zones
InfluxDB stores everything in UTC, and that is fine. Grafana shows it in local time anyway. The only place where I have to think about it is a daily total, because there I decide where a day ends:
- Hourly values are no problem. Grafana groups them into local days or months.
- The daily energy balance is cut at midnight in Vienna (
option location = timezone.location(name: "Europe/Vienna")). A UTC day would end at 01:00 or 02:00 local time, and the night consumption would land in the wrong day. - The daily PV yield uses UTC days. That works because there is no sun between 22:00 and 02:00 UTC 🙂
How easily this goes wrong, I found out while writing this post. An old task from my early days wrote the daily export as a running total, and aggregateWindow stamps its values at the end of the window. So the total of each day carried the timestamp 00:00 of the next day. A Grafana panel took the biggest value per day and summed them up, and after every sunny day it counted that day twice. It showed 537 kWh of export since 2024. The correct number is 470 kWh. No way! The task is deleted now, and all newer ones stamp at the start.
Grafana
The dashboard I look at most is “Current Power”:
- the production of both inverters and the yield of the day
- the consumption with the price of every quarter hour behind it
- gauges for the current power and the inverter temperatures
- the highest quarter hour of the day and of the month
- self-consumption and self-sufficiency, for today and for the last months, calculated in the panel from the daily values, so they are right for any time range

The red band around one o’clock is the boiler, switched off for a few minutes to keep the quarter hour below 3.5 kW.
The dashboards themselves I build in the UI, that is what Grafana is good at. The data sources are a file in provisioning/datasources/, again with a read-only token of its own:
apiVersion: 1
datasources:
- name: InfluxDB
uid: abcd1234
type: influxdb
url: https://influxdb.example.org
isDefault: true
editable: false
jsonData:
version: Flux
organization: home
defaultBucket: openhab_db
secureJsonData:
token: "..."
The uid is the one the data source already had, because every panel refers to it. With a new one, all dashboards would lose their data.
And the best part: with years of raw data in InfluxDB, I can test a new rule against real history before it goes live.
















