Reads household utility meters over radio and shows what the house actually used, by hour and by day, on a private web page the whole family can open.
Water works today. Electricity is written and waiting on my distribution operator to switch the meter's radio on. Gas needs a pulse sensor I have not fitted yet.
Live demo: https://home-utilities-demo.pawels-apps.workers.dev, password
demo. Made-up household, a new reading every hour, and the headcount buttons
work, so go ahead and change them.
Today's consumption, the headcount for the day, and the cards that compare this week and this month against the same stretch of the one before.
Weekends are tinted, public holidays get their own colour, and days the meter missed are drawn as estimates rather than passed off as measurements. Polish and English, light and dark, all switched from the header.
Any past day, hour by hour. The calendar follows the language chosen in the panel rather than the one the browser happens to be set to.
Screenshots are from the demo data, not a real household.
A meter counts upward forever. It never tells you what a Tuesday cost. This project reads the counter every hour and does the subtraction, so you get:
- consumption per hour for any day you pick, with hours nobody measured left blank rather than guessed
- this week against the same days of last week, and this month against the same days of last month
- litres or kWh per person, using a household size you set in the panel, because ours swings between two and six people depending on who is visiting
- Polish public holidays marked on the charts, including the Easter-based ones, and a card comparing holidays and weekends against working days
- Polish and English, switched from the header, with dates, decimal separators and plurals following whichever one is picked
- one Telegram message a morning with the last 24 hours, and an alert the hour a reading fails because the dongle vanished or the meter went quiet
Everything sits behind one shared password. There are no accounts to manage, which matters when the people using it are my family.
Two halves that talk over one HTTP endpoint.
The agent runs on a Linux machine near the meters. A systemd timer wakes it hourly, it listens for a single wireless M-Bus frame, writes it to a local file and posts it to the panel. If the panel is unreachable the reading is still on disk. A second timer sends the daily Telegram summary, built from those files rather than the radio, so it works even when the dongle is busy.
The panel is a Cloudflare Worker with a D1 database. It stores raw readings, derives the per-day and per-hour tables from them, and serves the dashboard. On the free plan this costs nothing: one reading an hour is 24 rows a day.
Readings are never edited in place. The day and hour tables are rebuilt from the raw readings whenever something changes, so correcting the household size for last Tuesday recalculates that day's per-person figures immediately.
Built in Poland against real meters, so the notes name real companies:
- Water. A Diehl IZAR radio module, the kind the local wodociągi fit as standard when they swap a meter. Mine came from the water utility in Starachowice. It broadcasts the counter in clear text, so there is nothing to request from anyone and no key to wait for. This is the one that works today.
- Electricity. An Apator Otus 3 with a wireless M-Bus HAN interface, the meter PGE Dystrybucja fits here in Świętokrzyskie. The radio arrives switched off and the frames are encrypted, so the operator has to enable the interface and give you the AES-128 key. PGE Dystrybucja accepts a written request and has two months to answer, free of charge. Tauron, Enea and Energa carry the same obligation under the same law.
- Gas. A Metrix (Apator) G4 with no radio at all, which is what tariff W-2 households usually have. It needs a pulse transmitter on the index dial instead, so it is the one still waiting on parts.
Nothing here is specific to those two companies. Any meter that speaks wireless M-Bus on 868 MHz works the same way, and anything else can post readings to the same HTTP endpoint.
Short version: a Linux box that stays on and a 15 EUR RTL-SDR dongle covers water and electricity. Gas needs a pulse transmitter and an ESP board instead.
Full list, including which dongles to avoid and why: docs/hardware.md.
cd panel
npm install
cp wrangler.jsonc.example wrangler.jsonc
npx wrangler d1 create utilities-db # paste the id into wrangler.jsonc
npx wrangler d1 execute utilities-db --remote --file=schema.sql
node scripts/hash-password.mjs 'household password' | npx wrangler secret put PASSWORD_HASH
openssl rand -base64 32 | npx wrangler secret put SESSION_SECRET
openssl rand -hex 24 | npx wrangler secret put INGEST_TOKEN # keep this, the agent needs it
npm run deploySet UTILITIES in wrangler.jsonc to the ones you actually have, for example
"water,electricity". The switcher at the top of the page hides itself when
there is only one.
To see the dashboard with data before any meter is wired up, put the same three
secrets in a local .dev.vars and seed it:
printf 'PASSWORD_HASH=%s\nSESSION_SECRET=dev\nINGEST_TOKEN=dev-token\n' \
"$(node scripts/hash-password.mjs 'demo')" > .dev.vars
npx wrangler d1 execute utilities-db --local --file=schema.sql
npm run dev
node scripts/seed-demo.mjs http://localhost:8799 dev-token water 60Delete the samples afterwards with DELETE /api/readings?test=1&u=water.
On the Linux machine, as root:
git clone https://github.com/pawisoon/HomeUtilitiesConsumptionMeter.git
cd HomeUtilitiesConsumptionMeter/agent
sudo ./install.sh --deps # installs rtl-sdr, builds rtl_wmbus and wmbusmetersThen fill in /etc/home-utilities/config.env with the panel address and the
ingest token, and create one meter file per meter. To find out what your meter
calls itself, listen for a couple of minutes and read the output:
wmbusmeters --listento=t1,c1,s1 rtlwmbusEvery meter within radio range shows up, so expect the neighbours. Match the id
against the serial printed on your own meter. Copy the driver name and id into
/etc/home-utilities/meters.d/water.conf, then:
/opt/home-utilities/bin/read-meter.sh water
systemctl enable --now home-utilities-read@water.timer
systemctl enable --now home-utilities-report.timerWith one dongle and more than one meter, give each meter its own minute so they do not fight over the radio. The timer template fires at three minutes past the hour, so move the second one:
mkdir -p /etc/systemd/system/home-utilities-read@electricity.timer.d
printf '[Timer]\nOnCalendar=\nOnCalendar=*-*-* *:33:00 UTC\n' \
> /etc/systemd/system/home-utilities-read@electricity.timer.d/override.conf
systemctl daemon-reloadThe empty OnCalendar= clears the inherited schedule before setting the new one.
Per-meter notes: water, electricity, gas. To run a public demo of your own, see docs/demo.md.
The database, the API and the dashboard all take a utility name, so adding one is configuration rather than code:
- Add it to
UTILITIESinwrangler.jsoncand redeploy. - Drop a new file in
/etc/home-utilities/meters.d/and enable its timer.
water, electricity and gas are defined in
panel/src/utilities.ts with their units and rounding.
Anything else means adding an entry there.
Databases created before the utility column exists need one migration, which labels the existing rows as water:
npx wrangler d1 execute utilities-db --remote --file=migrations/0001-single-to-multi-utility.sqlIf you ran that migration before 2026-09-18, run the repair as well. The first version of it copied per-person figures that were held in litres into a column that holds cubic metres, so the holidays card showed numbers a thousand times too large:
npx wrangler d1 execute utilities-db --remote --file=migrations/0002-fix-per-person-units.sqlWaiting also works. Every day row is derived from the raw readings, and the nightly job rebuilds the last 45 days.
agent/ reader for the Linux machine: scripts, systemd units, install.sh
panel/ Cloudflare Worker, D1 schema, dashboard
docs/ hardware and per-utility setup notes
The holiday calendar is Polish only, though panel/src/holidays.ts is a single self-contained file if you want to swap in another country's.
The agent reads wireless M-Bus meters. Anything else needs its own reader that posts to the same endpoint, which is one HTTP call:
POST /api/ingest
Authorization: Bearer <INGEST_TOKEN>
{"utility": "water", "timestamp": "2026-09-10T06:03:00Z", "total": 12.681}
The rest of the endpoints are in docs/api.md.
Hourly readings on a shared dongle interrupt whatever else uses it, briefly, once an hour. With a dedicated dongle there is no interruption.
MIT. See LICENSE.


