Self-built · Energy & home automation

The house that's learning to think about itself

How a logging server of tens of thousands of lines of code, built with an AI as co-author, transforms a Homey installation from "does what you tell it" into a system that measures, explains, and proposes improvements to its own behavior — complete with a digital twin of the house, controlled self-experiments, and a battery that knows when electricity actually costs money.

Living overview of the Homey_Log project — updated with every notable expansion Last updated: 13 September 2026
2,796
commits since June 12
105k
lines of server code
116
database tables
380
modules in routes/
Key figures for the codebase as of the latest update — the entire system runs alongside the existing Homey installation, not inside it.

Homey acts, but remembers nothing

A Homey installation is surprisingly capable: flows that react to price signals, a charge point that follows the sun, a home battery that fills up overnight during the cheapest quarter-hour. What a Homey doesn't do is remember. There's no place where "what happened yesterday" meets "what it cost" and "whether it even made sense". Every flow is an isolated little reflex; nobody — human or system — sees the whole picture.

Homey_Log is the layer that fills that gap: a Node.js server with its own SQLite database running alongside the Homey, recording everything that happens, layering a 3D twin of the house on top, and then turning an AI analyst loose on it to work through the numbers daily — and weekly, in greater depth. Not to operate the thermostat itself, but to say: "this pattern is off, you're losing money here, want me to adjust this script?" — and, under strict conditions, to run a small, reversible trial of its own.

Architecture at a glance

Homey_Log is an Express server (Node.js) with better-sqlite3 for storage, running under PM2 on a host of its own. Homey itself remains the source of truth for devices, flows, and logic variables — Homey_Log talks to it through two channels that are deliberately kept separate:

  • Push, from HomeyScript. Scripts already running on the Homey platform (charge planning, price calculation, dishwasher timing) send a log line to POST /api/log after every run. This is the primary data source: no polling, no delay, and the text of the log line is exactly what the script itself decided — traceable all the way back to the source language.
  • Poll, from the server. For anything Homey_Log needs to actively fetch or be able to operate, the server talks to the local Homey Web API with a Personal Access Token. Smart-meter data (P1), for instance, is polled directly on the local network (routes/hw-p1.js) rather than via Homey's cloud path — faster, and independent of whatever load Homey itself is under.
Homeyflows · scripts · logic variables · devices
│ HomeyScript push (/api/log) + local Web API poll (P1, device state)
Homey_LogExpress + SQLite — dozens of tables: log, quarter-hour/day/month energy, device state, rules
│ daily & weekly review — same tables, read-only SQL tool
AI analysis layerschema guide + house profile + rules + learned lessons → LLM (Anthropic / OpenAI / OpenRouter / local)
│ proposal (observation / action) — never automatic unless explicitly approved
Proposals and Experiments tabsMarc approves or rejects
│ on approval: written via the Web API, always allowlisted, always reversible
back to Homeya logic variable or a flow on/off — never a free-form command
The loop always closes with a human in it — except for an experiment's abort path, which by design is always allowed to intervene automatically.

The admin environment where all of this comes together is a single page with a two-row navigation: five groups along the top — Energy, House, Agent, Sources, and System — and below them the pages of whichever group you're in. Something lights up in both rows, so you can always see where you are and what it belongs to. The layout follows the question you're asking: what is my energy doing, what is my house doing, what does the system make of it, what happened at the raw level, and how is the app itself holding up. Every analysis page opens with a single line explaining what you're looking at — for instance, that a gap in a climate line is a period without measurements rather than a value of zero, and that the water meter covers the whole house while the shower counter only tracks a single pipe. That explanation sits above the chart, not below it: below, you only read it after you've already misread the lines. The texts ship as defaults in the code and can be customized per page in Instellingen ("Settings"), where only a modified line is stored separately — whatever you leave untouched simply moves along with the app. The admin environment opens on the page where you left off, unless the link itself names a page: a shared /#voorstellen always beats that memory.

That admin environment is one of three entrances, each with its own audience. The desktop site is the full toolkit: 33 pages, all the detail, all the buttons. The wall tablet in the dining room runs the family dashboard — big tiles, no admin. And on the phone there's the "Thuis" app ("Home"), which distills the house down to what you want to know while you're out. The rule between them is strict: the phone never shows anything the main app doesn't have. Parity or less, never more — otherwise you end up with two truths about the same house.

Signing in, per family member

The app has personal accounts. Each family member signs in once per device and the app remembers it; what you see and may do from then on depends on your role. Those roles form a single ladder of four rungs, and each rung includes everything below it: lezen ("read") is looking only; huisgenoot ("housemate") adds the comfort things — lights, moods, curtains, the vacuum robot; bedienen ("operate") may control everything, from the car and the dishwasher to the water heater and the operating modes; and admin is the full toolkit: settings, rules, accounts and the technical sources. Anyone who views or operates simply never sees the admin pages: the navigation shows only what belongs to your role. The conversation with the analyst is separate from all that, switched on or off per person — a chat window with the entire house as its context is something you grant deliberately, not automatically.

That ladder lives in one place, and "may this happen?" is a single pure function with no database or environment around it: an unknown role gets nothing. A route states its requirement through a guard — at least housemate, at least operate — and a handler then asks at most whose row this is, never whether someone is allowed. The browser doesn't even know the role names: on signing in it receives only the list of rungs this account reaches, and hides whatever doesn't belong to them. With no profile, not a single control button is on screen — and the server refuses anyway, because a hidden button is convenience, not security. Three tests enforce this mechanically: every write route is either protected or on an open list with a stated reason, every route genuinely refuses the rung below it, and every control button carries its own class on each of the three screens.

Everything under /api sits behind that same gate, reads included. The open list is short and argued line by line, and it holds the two exceptions that exist on purpose. The wall tablet in the dining room has no login screen — a wall display nobody operates anymore because a password has to go in first has lost its reason for being. And the entrance through which Homey itself delivers its log lines stays open: it has to keep working at moments when nobody is watching, or the data source goes silent without anyone noticing.

01

Record everything — the house's logbook

The foundation is a simple logs table: timestamp, source, level, message, a JSON data bundle and a run_id to group repeated runs of the same script. Alongside it sits a string of specialized tables that aren't arbitrary, but are the scars of months of "turns out that's not quite right": energy_quarterly (quarter-hour data since 2026, the finest grain), energy_daily and energy_monthly for older years where no quarter-hour data exists, device_events/device_state for individual device signals, variable_changes for logic variables, and entity_history, which detects changes to flows and scripts themselves — so the AI layer can later say "this behavior changed on the same day you modified that flow".

Ground truth over derived tables

A recurring pattern in this project: a derived table looks current, but has quietly stalled. The cars' charging status, for instance, lives in variable_changes, but that table can lag hours behind the raw HomeyScript log lines from the same hour. The principle behind it: when in doubt, don't trust the tidy derived table — go to the raw log line the script itself wrote. That's literally listed as a method item in the AI's internal schema guide.

To keep the log table from silting up with repeated, bulky warnings (a recurring bug can log the same payload hundreds of times a day), every data field is hard-capped at 400 characters per row — a small detail, but it prevents a very real "prompt too long" problem when the AI layer reads such a logbook.

How important is a log line? — the fifth level, "unknown"

Every line in the logbook gets an importance rating: debug (routine), info (an outcome), warning or error. The problem was how that importance got assigned. Devices speak their own dialects — the charge point says things like "WARNING" or "COM" — and anything that wasn't exactly one of the four known words was silently filed away as routine. As a result, 74 genuine warnings from the charge point sat invisible for months: the log view doesn't show routine entries, so nobody knew they were there.

Now a single place decides, in three steps: if the device itself uses one of the four words, that stands. If not, a list of translation rules kicks in ("if the charge point says WARNING, that's a warning"), and Marc can manage those rules himself in Instellingen (Settings). And if nothing matches, the line gets the fifth level: unknown. That's not an error state but a to-do list — Instellingen shows which unknown kinds are coming in, and Marc decides per kind how important it is. The device's original term is always preserved, so a wrong call can be corrected later: he can adjust the rules and rerun them over the stored log lines.

The rules are evaluated in order: the first match wins. That means a rule further down the list can silently never get its turn — and that's exactly what happened. A cleanup pass found six of the 39 rules doing nothing at all: entered twice, or shadowed by a rule above them. The system now refuses to save such a rule and tells you which rule already catches it, so that the list you read is genuinely the list that runs. Only provable cases get refused; when in doubt, the system lets the rule through, because a wrongful rejection is worse than a redundant rule.

The underlying philosophy is the same one that runs through the rest of the system: better to say "I don't know" out loud than to let a silent assumption harden into fact.

Logical vs. physical devices

When a broken smart plug gets replaced, Homey assigns the new unit a brand-new device ID — which would reset all reporting to zero, as if it were a different device. To the household, though, it's still just "the TV's plug". So there's a thin layer in between: every physical Homey device hangs off a logical device (logical_devices + device_links), and settings like "count toward consumption" and "should always be on" belong to that logical level. The raw measurement data stays untouched at the physical level; the logical layer is purely a reading lens on top. 👤 When a device is swapped, Marc marks the old one in Instellingen as "replaced by…" (deliberately a manual confirmation, never automatic guessing), 🤖 after which history and settings carry on seamlessly across the swap — while it stays visible when one physical device handed over to the other, with the full measurement series presented as a single continuous device. Every reader goes through this layer: the Usage tab and its drill-down, the gas-free-effect model, the presence history, the standby-power tracker and the 3D house — the last one arranged so that a stale device reference in the 3D model just keeps working (status and control are quietly routed to the replacement). The device table itself lives in Instellingen → Apparaten (Devices).

One glitching meter must not drag down the rest

The quarter-hour sampling script on Homey samples three things in a single run: the charge point, the heat pump and the car's battery level. Those three sub-measurements are decoupled: each catches its own errors, so an unreadable source produces an empty field only for itself while the rest carries on. On such a failure, the last known meter reading is deliberately left untouched, so the missed energy arrives as a catch-up sum on recovery instead of evaporating — which keeps the daily total honest at all times. The repo also keeps a copy of this script (scripts/homey/Sample_EV_Charge.js), so changes to it can be reviewed as a proper diff.

Because Homey resends the entire buffer every 15 minutes, the ingest keeps only the first, regular measurement per quarter-hour within a single delivery. That way an extra (say, manual) test run in the middle of a quarter-hour can't echo a second measurement for that same quarter-hour into a full day of repeated "suspicious: value overwritten" warnings.

Retention: how long does anything stick around?

All that recording also means the database grows every single day and never cleans up after itself. For that, a configurable pruning system runs. It doesn't reason per table, but per role a table plays in the house: is this the irreplaceable measurement foundation, is this high-frequency diagnostic noise, is this a rare meaningful event, or is this something that should never be thrown away? Each role gets its own retention period; a new table added later simply picks one of those four roles and doesn't need to invent its own period.

There's a safety net built in: every role has a hard floor, tied to what's actually done with the data — for example, "the Logs tab lets you read a year back". Accidentally set a period shorter than that floor, and the table is simply skipped rather than pruned — so a setting that's too tight can never quietly break a feature.

The motivation was a painful lesson: an older, blunt rule deleted everything older than two years without discrimination — and in doing so wiped out the solar-yield history that had been painstakingly recovered back to 2018 from the inverter API. Well over two hundred thousand measurement rows vanished in one sweep; the solar archive temporarily started only in mid-2024 as a result. That is precisely what this system must never allow again: one blind rule for everything has been replaced by a deliberate choice per role, with a floor of 4,000 days specifically for this series. The wiped history itself is back: 231,479 quarter-hours re-fetched from the SolarEdge API, without touching any of the data collected in the meantime. The solar series runs from January 2018 and the year tiles are correct across the entire range.

👤 Marc sets the mode per role: off, dry run (the system computes and logs what it would delete, without touching a thing) or active. 🤖 Every night, well before the morning's consumption, the system applies whatever is configured. The measurement foundation and the meaningful events are set to off — nothing gets deleted there. Only the diagnostic noise is on dry run with a one-year period: every night the system calculates what it would prune and writes that down, without touching a row. That way there's a number to base a decision on before anything actually disappears. The controls live in Instellingen → Gegevens & systeem → "Retentie" (Retention).

02

The digital twin: the house in 3D, live

On top of the log data sits a full 3D model of the house — both floors, rooms, furniture, windows, dormers, right down to the toilet windows and the nook under the stairs — captured in a house-model.json thousands of lines long and rendered by a custom client script. This is not decoration: it's a live layer on top of the Homey Web API. Devices take on the color of their actual state (a lamp that's on glows, a room that's too warm gets a different temperature color), and you can control things directly — clicking toggles a device on or off, right-clicking opens a panel for dimming, color, white tint or a camera frame.

3D model of the house, roof view with solar panels, in the House tab of the desktop app
The House tab: roof view with the solar panels visible. The layer buttons follow the order of the house itself, top to bottom — Dak (roof), Zolder (attic), Eerste verdieping (first floor), Begane grond (ground floor) — and together with the Live/Comfort toggle switch layers on and off; clicking a room zooms into it.
First floor isolated (roof, attic and ground floor switched off): rooms in their own pastel colors, with furniture
With the roof, attic and ground floor switched off, only the first floor remains — this is where the room colors and furniture really come into their own.

Beyond toggling layers, the view also has an Exploded mode: a single button takes the house apart, laying out roof, attic, first floor and ground floor as separate panels side by side on the ground. That lets you look into every layer at once — handy for spotting at a glance which lamp is still on somewhere in the house, without peeling it apart floor by floor.

For reading and writing, the server deliberately uses two separate Personal Access Tokens (routes/homey-client.js): a read-only token for the constant status poll, and a separate write token that's only touched on explicit click actions, with a hard whitelist of which capabilities (on/off, dim, color, color temperature) may be driven from the twin. Nothing outside that list is reachable from the 3D view.

Why this is more than a gimmick

The house's time chart (uPlot, dual y-axis) shows not just energy flows but also an event overlay: control actions from the twin appear as markers on the same timeline as the energy consumption. That turns "why did consumption spike at 21:40" into something traceable to a concrete action in two clicks.

That overlay fetches exactly the levels that are switched on. It sounds like a detail, but it's the difference between a measuring instrument and a false comfort: if the tab fetched a fixed number of rows and only filtered in the browser afterward, the budget would be spent on routine lines and precisely the errors would drop out — and because the oldest rows arrive first, the most recent part of the window would be missing. The tab could then show "no events" while errors do exist. If a window still doesn't fit in one pass, the status line says up to which moment you're looking rather than merely that a limit exists, and the server writes a warning to the logbook so the daily analysis sees it.

03

From data to euros — what it costs you and what it earns

Raw quarter-hour data is worthless without a consistent calculation model on top. That model — which kWh counts as "import", when solar power counts, how a negative PV signal gets ignored — lives in a single module (routes/energy-model.js) that every derived screen reuses, guarded by a parity script that compares server-wide aggregates against the module's output (npm run parity, always a 0.0000 difference). The rule of thumb behind it — "consistent beats exact" — means a month is always exactly the sum of its days and a year exactly the sum of its months, even if that drifts a few kWh from some standalone, older source document.

That core is deliberately fenced in. Which measurement series is "solar", which is "battery", which is "grid" and which is "charge point" is derived from a single table in the mapping layer; the calculation module itself knows only those concepts. The historical series names still carry a brand name — renaming touches dozens of names, dozens of tests and the scripts on the Homey — but only that one mapping file is still allowed to spell them, and a test turns every other place red, with an exception list that may only shrink. Reading from and writing to the quarter-hour table goes through a single door; raw queries outside it are on a list, each with a reason. The goal is entirely concrete: a third home battery ought to be one line in the mapping.

On that foundation sit tiles and pop-ups that reckon in concrete euros rather than bare kWh:

ViewWhat it shows
Self-sufficiency / direct-solarwhat share of consumption never came from the grid, measured on the main meter's own import. The car and the hot-water boiler stay out of the headline percentage — they deliberately charge on price, not on sun, and would flatten a figure about the house every charging night. The chart below it splits that same percentage into what the whole house genuinely achieved and what only appears because those two devices sit outside the sum
Effect tile (solar + battery + gas-free)euro effect per full month of three measures combined — it can come out negative when the telemetry has a gap, and it is shown that way rather than guessed as zero
Standby drainbaseload = the median of the lowest-hour house consumption per day (excluding solar/battery), plus a top-10 of likely culprits with payback times, with a hard exclusion for the fridge/freezer/server/alarm and the like
Charging price per caraverage charging cost per kWh over the chosen period, split per car via the HomeyScript log that tracks which car is actively charging — a good 24% cheaper than the bare grid price through smart timing. It lives in the popup of the car tiles, per car: that car's charging sessions, the solar/grid split and the current state, with a single line below showing the average charging price and how far it deviates from the average grid price. The charging-plan utilization (hindsight benchmark) remains available in the API but is no longer shown
Gas-free effectgross kWh of the heat pump and hot water (the boiler; via the logical-device merge one continuous series with the earlier instant shower heater) converted to a hypothetical gas scenario, using current gas prices from the EnergyZero API
Which kilowatt-hour actually came from the grid?

Self-sufficiency long looked like simple arithmetic, but a choice hides inside it that can shift the answer by tens of percentage points: if the car is charging and the house is running in the same quarter-hour, and the meter reports import — whose electricity is that? The answer now comes from the meter itself, not from a reconstruction. Each quarter-hour, the measured import is distributed: the house first gets its own, unmetered residual load, then the deliberately excluded devices (car, boiler) take at most their own measured energy. That way a charging session cannot pollute the house figures, and the house cannot pollute the charging figures either.

A remainder is left over that logically cannot exist: import above the total house load. It is real and measurable — the smart meter's clock runs about a quarter-hour ahead of the devices', so a charging peak shows up on the main meter slightly earlier than at its source. That surplus therefore follows the neighboring quarter-hours: if there was charging activity just before or after, it belongs to the culprit, otherwise to the house. The boundary guarding all this is a hard one: house import plus device import always equals the measured import exactly, and neither can go negative — not even when a counter jumps backward.

The Power tab itself is one continuous timeline (uPlot) that picks its own granularity — quarter-hour, day, month, year — instead of a separate "history" section: less navigating, and the app decides for itself when the detail becomes too fine to display meaningfully.

Historical solar back to 2018. The full production history sits in the database at quarter-hour resolution, back to the installation in January 2018, fetched through the SolarEdge API. Both inverters (garage since 2018, attic since April 2019) hang behind a single SolarEdge site, while the database knows them as separate fields; a "split" mode therefore divides the site series per month according to each inverter's cumulative production counter at the month boundaries (3 API calls per month instead of hundreds). Counter readings and a high-water mark are cached so a backfill resumes where it left off after hitting the SolarEdge daily limit (300 calls/day); existing data is left untouched (INSERT OR IGNORE). The yield reconciles to within ~2% of the SolarEdge portal, and the live pipeline matches the inverter counters to two decimal places — which makes SolarEdge the independent ground truth for solar production.

Boiler tile: solar versus import, down to the session. The boiler tile in the Usage tab splits consumption per day into two streams — what ran on solar surplus (free) and what came in as grid import (paid, at that moment's quarter-hour price) — with a bar chart of the chosen period. Below it sits the list of individual charging sessions that did contain import: per session the time window, the kWh, how much of it was import, and the cost at the quarter-hour price. Sessions that ran entirely on solar cost nothing and therefore don't get their own row — only a single total line ("N session(s) entirely on solar, X kWh — free").

Two snapshots in the Usage tab: battery charge and boiler water. Alongside all the period figures there are two tiles that simply show now. The Thuisbatterijen (home batteries) tile shows the average fill level of the two Sessy batteries, with a fill bar and each battery's individual percentage; clicking opens today's curve per battery and the charge/discharge efficiency since measurements began. The Boiler tile keeps its cost figure but adds the current water temperature (with a shower icon whenever someone is showering at that moment), and its popup opens with today's temperature curve in which the measured showers appear as blue bars — you immediately see the dip a shower punches into the boiler. Both numbers come from exactly the same source as the wall tablet, so the two screens can never disagree. They deliberately do not follow the period picker: this is a snapshot, not a period analysis, even when the selector above is set to "maand" (month) or "jaar" (year).

Boiler water temperature, genuinely measured. The boiler has a Shelly ("Boiler Control", zone Zolder) that measures the water (measure_temperature.1); that reading feeds both the big number on the dashboard tile and the analysis tab "Boiler" in the Energy group, with the same standard time dimensions as the rest of the app (hour/day/week/month/year), a chart with an average line and, if you like, a subtle min/max band (the boiler's daily heat-up-and-cool-down swing), a table and CSV export. The series runs from its first reading and has no retroactive history — it fills forward. This measurement is the foundation under the water-heater control described later on: making hot water on solar surplus and on cheap quarter-hours instead of on a fixed schedule.

A watchdog on the boiler reading, and why it must not be jumpy. Alongside the measurement runs a watchdog that 🔔 sends a message if the boiler reading stalls. It checks three things: are new readings still coming in, do they contain a value, and is that value actually fresh (Homey will happily keep serving a dead sensor's last value forever as if all is well — see the lesson about that further down). 👤 Marc got a notification a few times a day that the temperature was "unreadable". 🤖 Recalculation showed nothing was wrong: Homey occasionally returns one stray reading with a fresh timestamp but no value, and a minute later the same number is back. Nine such hiccups in ten thousand readings over a week, and because the watchdog based its verdict on a single reading, each hiccup produced two messages — the alarm plus an "all clear". Eighteen messages for zero actual failures. The watchdog now waits for three failed readings in a row before it says anything, and no longer sends an "all clear" for a failure that was never reported. One important detail: a stalled measurement is still reported immediately, without waiting for those three — if no readings arrive at all, such a run of three would never fill up, and the most serious case of all would stay silent.

Shower water pulses: from pulses to liters per shower. The same Shelly now also counts the water pulses on the shower's hot-water line (input_pulse_counts_total). Homey already recognizes with a flow that someone is showering (and cranks up the heat-recovery ventilation); Homey_Log hooks into that: on shower activity the server reads the cumulative pulse counter live itself, opens a session and then watches on its own for the end — once the counter stops climbing for a few minutes the session closes (so soap breaks and a brief tap pause still count as one shower).

Why a short shower would come out far too low. 👤 Marc noticed that some showers logged "VERY few liters". 🤖 Recalculation traced the problem to the baseline: Homey's notification structurally arrives about a minute and a half after the tap opens, and the water that had already passed the meter by then fell outside the session. In liters that loss is fairly constant — always that same minute and a half, some 3 to 5 liters — and therefore devastating for a short shower: one shower was logged at 1.3 liters when 5.5 actually went through the meter. Across all showers, 15% was invisible. The session therefore now starts from the counter reading the per-minute measurement had already captured two minutes before the notification. Why exactly two minutes? Run against every measured shower, one, two or three minutes give exactly the same result, because the counter sits idle for hours between showers — two minutes lands safely in the middle. The end of a session needed no adjustment: it was demonstrably complete already, because the server keeps watching for another three minutes after the last increment. The raw measurement stays in the database next to the corrected one, so the intervention is verifiable and reversible. The fifteen showers from the measuring phase were recalculated: 52 liters that previously belonged to no one's session are now accounted for.

The whole-house water meter. Next to the shower counter — which only sees the water through that one line — a water meter with its own network connection hangs in the meter cupboard. By default the server asks it directly for its counter reading every ten seconds, exactly like the smart electricity meter. How often that happens — and how often a row gets stored — is configurable, and a change takes effect immediately without restarting the server. Deliberately not through Homey, because Homey keeps repeating the last known value when a device goes silent — and a water counter that stands still is then indistinguishable from a house where no water happens to be running. Consumption is recorded per quarter-hour in its own table; liters don't belong in the table where the kilowatt-hours live.

A leak betrays itself by what it doesn't do: stop

Water that runs uninterrupted for a long time is the signature of a running toilet float or a leak; a shower (about seven minutes), the washing machine and the dishes all stop on their own. If more than half a liter per minute flows uninterrupted for over three quarters of an hour, Homey_Log sends 👤 Marc a notification. A detected leak then reports itself at most once every six hours, so one dripping tap doesn't become a flood of messages. The thresholds live in Instellingen (Settings). Nothing gets shut off or turned down here — the house only warns.

What "uninterrupted" means was measured rather than assumed. A running episode turned out to be regularly cut short by a dip of exactly zero liters per minute — not a matter of an over-sharp threshold, since the dip sits at zero. Most likely it's the meter itself skipping a beat. An interruption shorter than ten minutes (configurable, "Dip mag") therefore no longer resets the clock to zero; if it lasts longer, the count starts over. That lets an episode accumulate honestly and eventually report "days" instead of starting from zero every hour.

What the house meter is not, is a calibration of the shower counter — a fallacy this page itself used to contain. 👤 Marc pointed out that the two measure different things: the pulse counter sits on the shower's hot-water line, the meter in the cupboard counts all the water in the whole house, cold included. During a single shower the house meter therefore always reads considerably higher, and that gap is mostly the cold water the thermostatic valve mixes in — not, as this page used to claim, other consumption elsewhere in the house. As a check, only the crude direction survives: if the house meter ever sees less than the shower counter, something is thoroughly wrong. A real calibration takes one deliberate measurement: the shower fully on hot with no cold, and nothing else drawing water in the house — then the increase on the house meter is precisely the water that passed the pulse counter.

The house looks ahead: what will the sun do? Everything above is about measuring what already happened. To be able to plan — when the car charges, when the boiler heats, when the battery charges — the house also needs some notion of tomorrow. For that, Homey_Log fetches a solar forecast every hour, and stores it. The storing is the whole trick: a forecast can't be retrieved anywhere after the fact. What the house thought yesterday the sun would do today no longer exists tomorrow — unless you write it down. And it's precisely the gap between what was expected and what arrived that makes you smarter later.

A forecast that doesn't know the installation predicts something else

Homey already had a Forecast.Solar device, and it seemed to work. Hooking it up revealed why you have to verify such things: it described an installation of 3.6 kilowatt-peak on one roof face, while in reality 11.6 kilowatt-peak sits on four roof faces. Its forecast for that day stood at 7.6 kWh while 40 kWh came in. The service wasn't broken — it was configured wrong, and was dutifully answering a question about a house that doesn't exist here. Homey_Log therefore now queries the service directly, with the data the inverter manufacturer itself keeps about the installation.

That home-grown request does two things better than the off-the-shelf app. First, it asks per roof face: four orientations, two inverters, each with a large group of panels and a couple on the other side of the ridge. One averaged orientation cannot describe that. Second, it accounts for the inverters clipping: there is one and a half times as much panel power on the roof as the inverters can pass through, so on a bright afternoon the top simply gets cut off. That clipping applies per inverter, across two roof faces combined — and that is exactly what the off-the-shelf app cannot compute.

The whole installation lives in Instellingen and can be adjusted there: per roof face the orientation, the roof pitch, the number of panels and the power per panel. As long as 👤 Marc hasn't filled in a roof face himself, it visibly says schatting (estimate) — the house doesn't pretend to know something it guessed. The card puts today's forecast next to the actual yield so far, so a wrong assumption betrays itself instead of tagging along for years.

Correcting with what actually comes in. The service's own forecast is systematically off — 18 to 49 percent too low over the measured days — and doesn't correct itself as the day progresses. Ruled out as causes: the summation, the split into roof faces, the orientations, the declared power, and even the paid subscription (the forecast for tomorrow differs by 0.3 percent with the subscription versus without). It's the service itself.

What that subscription does offer: you may report how much has been harvested today, after which the service rescales the rest of the day. Homey_Log does that every round. The effect is substantial — on a bright day the expectation for that day jumps from 29.7 to 51.5 kWh, at a point where 39.2 kWh was already in by four in the afternoon and the low figure had long since been overtaken by reality. For tomorrow this changes nothing; that deviation is a separate story, and that is why a second forecast source sits alongside this one.

The correction happens per inverter, not per roof face, and that is a deliberate boundary: the inverter reports one total and cannot say which roof did what. Correcting per roof face would look more precise but would divide a measurement that doesn't exist. For the same reason the forecast is also fetched per inverter — two requests per round instead of four.

That second source is a free weather model asked the same question: Open-Meteo, and it is the source the house plans on. The choice fell on the uncorrected figures — which source performs best on its own — across the days where both gave a fair answer: 23.9 percent average deviation against 26.7. What counts here is the absolute deviation, not the average: a source that runs thirty percent high one day and thirty percent low the next averages out beautifully and is still useless. Only that winner gets a correction factor, learned from the house's own measurement series and overridable by hand in Instellingen — not baked into code, so falling back to the other source is a single switch. The paid service keeps running alongside as a comparison, because a source you no longer lay next to reality is a source you can no longer win back.

One quirk belongs to it and is deliberately not papered over: the factor works on the day, not on the hour. Within a day the forecast may therefore visibly miss while the daily total is right. That is the honest shape — the model knows something about how much sun is coming, and far less about exactly when.

And then the question that matters: was it right? Storing a forecast only pays off once you can lay it next to reality. The Power tab therefore offers two extra lines to switch on: what the house thought the evening before that the sun would do, and what it made of it as the day went on. The first is the line that counts — that is, after all, what you know in the evening when you have to decide whether the car charges tonight or tomorrow afternoon. The second sits next to it to show how much the weather model still adjusts along the way.

That brings a rule that holds in every chart in the app: solid is measured, dotted is forecast. A measured line stops at the last full hour — the hour you are standing in the middle of isn't finished, and drawing half an hour next to whole ones produces a kink that means nothing. That is exactly where the dotted line picks it up, so the handover from "this happened" to "this is expected" runs smoothly and still stays visible. Every line that is drawn appears in the legend, the forecast ones included: a nameless line is a riddle you get to solve yourself.

One percentage would hide where the problem lies

Next to the chart stands a verdict, and it deliberately consists of two numbers. The first is the bias: does the house systematically predict too low or too high? That says nothing about the weather — it says the roof data or the damping isn't right yet, and that you can fix. The second is the typical deviation: how far off the forecast is in any given hour, regardless of direction. That is the unpredictability of the weather itself, and it is the ceiling on what planning ahead can ever win you. Collapsed into a single percentage you would see that it went wrong, but not whether you could do anything about it.

Two things keep this honest. Hours with barely any sun — dusk, a winter late afternoon — don't count: a 300% deviation on a hundredth of a kilowatt-hour means nothing, but would dominate the average. And as long as there are too few usable hours, the screen shows no percentage but a note that there isn't enough data yet. An accuracy resting on three measurements is no accuracy.

The period picker in this tab also has something it has nowhere else: Morgen (tomorrow). Until now Homey_Log looked exclusively backward, and rightly so — there was nothing to show about the future. With a stored forecast that changes for the first time. In that window there is no measured yield to compare against yet, and the screen says so: this is an expectation, not a verdict.

One limitation, stated explicitly: the comparison starts on July 31, 2026. For every day before it no forecast was stored, so the lines stay empty there. They are deliberately not drawn at zero — the house didn't expect zero sun back then, it expected nothing.

For the same reason the house water doesn't appear as an extra line in the boiler chart, but in its own tab: two series that are both called "liters" but measure a different scope read as a comparison they aren't.

Pulses become liters through a calibratable key: Instellingen holds a card "Waterpuls-kalibratie" (water-pulse calibration) that says "now draw off a known quantity" and computes the liters-per-pulse from the counter's own start and end readings (recalibratable when needed, with history). Under the analysis tab "Boiler" sits a Douchesessies (shower sessions) block: a table with start, stop, duration and liters plus CSV export, and a daily total. It also shows each shower's temperature drop: how many degrees the boiler fell, measured from just before the shower to the lowest point just after. A short shower of 7.5 liters costs half a degree; a bath of nearly 49 liters cost 26. The boiler keeps cooling for a while after the tap closes, so the lowest point usually lands a few minutes past the end; Homey_Log looks no further than three minutes, because beyond that it's just sinking from standing heat loss and that is no longer the shower's doing.

One chart, three stories you toggle independently. The Boiler tab puts three series in the same chart: the water temperature, the liters per shower and the boiler's electricity use in kWh. Each has its own tick marks along the edge — degrees on the left, liters and kWh on the right — because liters and degrees don't fit on one ruler. The bar at the top has a checkbox per series, in that series' color: checking it adds it, unchecking removes it along with its tick marks. By default you see the temperature and the showers; whatever you toggle is remembered for next time. So one glance tells the story of a day: the boiler cools slowly, someone showers, the temperature dips further, and a little later the boiler draws power to heat back up. There is also a period setting "Uur" (hour), for when you want to examine a single shower up close.

One thing stands out, and it's meant to: where no temperature was measured for a while, the line shows a gap instead of a ruler-straight stroke between two readings. That gap is more honest — it says "I don't know here" instead of suggesting a curve nobody measured.

04

The AI that combs through it every day

The heart of the system is an agent pipeline (routes/agent-pipeline.js) that works through the data daily and — more deeply — weekly. The analyst doesn't get a throwaway prompt, but a carefully built context:

  • a house profile (house_facts): geometry derived automatically from the 3D model (floor area, volume, per room), supplemented with manually entered facts, and extensible from within — the AI may propose a new "fact" of its own when it spots something structural, which then waits for approval as a proposed fact;
  • a schema guide: a living document that spells out exactly which table is the source of truth for which question, pitfalls included ("never use daily_rollup for this one edge case") — effectively the tribal knowledge accumulated over months of debugging, so the AI doesn't have to rediscover it every time;
  • active rules and troubleshooting recipes that Marc has put on record;
  • a growing memory of learned lessons (more on that below) — things that did or didn't turn out to work in this house.

Three write paths, three destinations. The analyst isn't allowed to change anything about the house itself, but it can leave three kinds of notes behind — and those deliberately land in different places: a house fact (house_facts, awaiting confirmation under Instellingen (Settings)), a rule for its own interpretation (agent_rules), and also a proposal for a concrete change (proposals, tool propose_change) that simply shows up in the Proposals tab and passes through the same Tier 1 self-check as a proposal from the daily report. That last one exists for a reason: if you ask in chat to have something recorded, it should not be filed away as an agent rule — semantically wrong, and you'd never find it where you go looking. Proposals born from a conversation carry an "uit de chat" (from the chat) label, so it stays visible where they came from.

For the reasoning itself, the analyst has a read-only SQL tool over its own database — no fixed report format, but free-form querying, so a hypothesis ("does this pattern hold last month too?") can be tested on the spot. The language model itself is swappable: a custom abstraction layer (routes/llm-client.js) supports Anthropic, OpenAI, OpenRouter and a local model, with a separately configurable provider per task — daily pipeline, chat, self-check — so quality, cost and privacy can be weighed per task. Every call is logged with its token cost, all the way up to a dedicated "AI-kosten" (AI costs) tab with a stacked chart per task.

Self-learning

After every run the analyst may record up to three generalizable lessons — method lessons ("I should check this blind spot from now on") or outcome lessons ("this measure did/didn't turn out to work in this house") — and, when new data contradicts an earlier lesson, retract it on its own. Marc can always restore any lesson: the system is allowed to correct itself, but never out of sight.

Those lessons end up in the daily prompt within a fixed character budget, and that budget is guarded by two rules. At most two lessons per topic make the prompt, so no single topic can crowd out the rest; and Marc's own corrections go first, because the budget fills front to back and whatever sits at the end never reaches the analyst. Without those two rules, the pile wins: eight rephrasings of the same self-written conclusion jointly push every correction from Marc out of view, and the analyst ends up mostly reading itself.

For the same reason, the system keeps its own judgment and Marc's apart. If Marc rejects a proposal with his own reasoning, that's feedback and it carries real weight. If the self-check closes a proposal, that's a machine decision and no lesson is made of it — a system that stores its own verdicts as human judgment grows ever more confident in its own assumptions.

The echo chamber: a problem that keeps itself alive

The most dangerous thing a self-learning system can do is cite itself. One finding reported a "fifth occurrence in three weeks" of a fault that had long since been fixed — and the only evidence for it was earlier reports claiming exactly the same thing. The counter ticked up every day while nothing was happening, and the accompanying proposal kept itself alive for weeks that way. That costs more than time: the advice it produced was to file a warranty claim with a manufacturer for a defect that didn't exist.

Now, whenever a finding claims recurrence, there has to be at least one log line from its own analysis window to back it up. If none can be found, the "again" claim disappears and only the finding itself remains. Deliberately strict: a claim resting solely on earlier reports or on loose data points is neutralized too, because exactly that form was the echo. The price of a wrongful neutralization is small — the observation stays, only the counter goes — while the price of a missed echo is advice nobody can use.

The daily analysis runs at half past six in the morning, the deeper weekly review on Saturday morning. Per run, not just the model is configurable but also how much thinking it's allowed to do — and that setting is only sent to models that understand the concept, so a model that doesn't won't throw an error but simply produces a log line.

05

Proposals instead of a black box

A finding from the analyst is never silently acted upon. Every finding gets a kind: an observation (something caught its eye, no decision needed — just tick it off as "read") or an action (a concrete proposal with buttons: approve, reject with a note, or decide later). A badge in the navigation counts only the genuinely actionable proposals, so observations don't get in the way as noise.

Alongside Marc's manual review, that same pipeline offers three forms of controlled self-direction:

TierDoes
Tier 1 — auto-verificationautomatically closes proposals that are demonstrably wrong: a problem that came up before and healed itself, a title that no longer matches the date, a claim ("script X already has a guard against this") that the system checks against the actual script code, or evidence that can't be found in a single log line. That last one catches the case where the analyst cites its own earlier reports instead of an observation: the quoted text is searched back in the log by its longest digit-free part, and if that turns up nothing, the proposal is closed with that reason attached. Evidence that can't be substantiated from log lines — a data point from the database, say — is explicitly out of scope and never closes a proposal on this ground. The daily Pushover notification only counts after this self-check, so a proposal rejected seconds later doesn't come through as "1 new action proposal"; self-closed proposals are mentioned rather than counted
Tier 2 — experiment routingautomatically translates a testable hypothesis into a draft experiment, validated against the Homey variables that actually exist — executed only after approval in the Experiments tab
Tier 3 — script proposalsgenerates a complete new script proposal including a unified diff against the current script; a strict check rejects diffs that only touch comments or text, so no hollow "proposals" linger

One design choice made explicitly here: no separate "doubtful, judge for yourself" category for borderline cases. If something is demonstrably wrong, the system closes it outright — an in-between category would only add review work without adding value.

06

The house experiments on itself

The most advanced form of self-direction is the experiments stage: when a hypothesis meets strict conditions, the analyst may propose a targeted, time-boxed trial instead of merely reporting on it. The conditions are hard: the hypothesis must be measurable against a fixed list of KPIs (cost, import, export, solar yield, battery behavior …), the change must be on a short whitelist — only setting a logic variable to a value, or toggling a specific flow on or off — and the system must read in the starting value itself beforehand, so rolling back is guaranteed.

Around that core sits a set of safety nets, each of which grew out of an explicit demand from Marc:

  • a master switch, off by default — proposing and approving sit behind it, but aborting and rolling back always work, even with the switch off;
  • an A/B schedule that alternates day by day between the new and the old setting, so an effect can't be mistaken for chance;
  • an automatic emergency stop on guarded boundary metrics: if the cost price or the import consumption shoots outside a preset margin, the experiment is rolled back immediately, with a loud notification and a forced "this didn't work" lesson.
Example · running experiment LIVE

A home battery that is normally simply "parked" (switched off) while the car charges overnight runs the risk of being drained right into the car the moment it wakes up again. The experiment steers the battery in that situation not to a standstill but to a balancing point of "zero plus the car's charging power" — via a parameter on the battery control system that normally balances toward zero on the meter, but here is handed a shifted target. That control mechanism is sign-correct: a positive value leaves the meter showing exactly that much import, a value of zero balances back to zero-on-the-meter. Cost price and import consumption are the guarded boundaries that will roll the experiment back automatically if need be.

Along the way, exactly the kind of fault surfaced that this whole system exists to find: a nightly charging session failed to start despite a correct charging plan. Digging through the logs exposed a gap in the execution chain — the flows that were supposed to actually switch on the charging station had been off for weeks "because a script would take over that role", a handover that in fact was never completed. Found within a day, fixed, and proven working on the real charging station within two minutes of the fix.

07

What Homey already handles on its own — and how the AI layer watches along

Homey_Log observes and suggests; it is not where the day-to-day energy control actually happens. That control runs, as befits an extensive Homey installation, in a network of flows and HomeyScripts. The next chapter is about the place that takes this work over once it has proven it does it better; this chapter is about what is genuinely at the controls today. The main pieces:

Smart charging on day-ahead prices

A daily planning script slices the night into quarter-hours and assigns each one a strategy — charging, discharging, holding, or a separate regime for quarter-hours with a negative price — for both home batteries as well as the charge point, with a hard deadline so the car always has the requested charge in time, even if that means ignoring the cheapest quarter-hours.

That plan is only as good as the battery level it works with, and there's a catch: a car that is asleep at the charge point stops reporting its state of charge, so the plan would otherwise be working with a days-old value. The remedy is an SoC poll on plug-in: the charge point first delivers a five-minute charging pulse (which wakes the car, after which the real battery percentage comes in on its own) and only then switches to solar mode. That lives in a single flow; all the planning scripts are left untouched.

Charge point over Modbus TCP

The charge point is controlled over Modbus TCP — which makes Homey the unit's energy management system. That deliberately bypasses the charge point's own web server, which had a memory leak fueled by a login every 30 seconds; over Modbus that login cadence is gone and the unit stays calm. The quarter-hour actuator is pause/resume with a current limit. On that foundation runs Green_Tick: a server-side controller that charges the car during the day purely on surplus solar power. Every minute it computes the available surplus (car draw + feed-in), translates that into a maximum current (6–16 A) and sends it to the charge point — a closed control loop, so once the car eats up the solar output the meter hovers around zero instead of flapping. The solar window runs until that day's actual sunset, computed from the location rather than a fixed clock.

Green measures per charging session whether the car charges on 1 or 3 phases — from the charge point's actual per-phase current during the first minutes — and scales its thresholds and current accordingly: a 3-phase car starts at around ~4.5 kW of surplus, a 1-phase car from as little as ~1.5 kW, and may draw up to 16 A on its single phase (~3.5 kW instead of being capped at ~2.3 kW). If a car normally charges on three phases while there isn't enough sun for three, Green can temporarily put it on a single phase, and automatically returns to three as soon as that is possible or necessary — always via a pause, because a phase switch mid-charge is not something a car appreciates.

The control is deliberately asymmetric: when a cloud rolls in, the power drops immediately, but before starting or stepping up Green first waits for a few minutes of sustained surplus, so a brief sunny spell doesn't trigger a session that has to be aborted a minute later. Pausing happens as soon as the surplus no longer covers the minimum charging power — the thresholds are derived from that minimum rather than configured separately, so there is no band where Green sees "enough sun" while the car at its lowest setting already draws more. 🤖 Green also watches over charging sessions it didn't start itself: if the car is charging during the day without the sun covering it, and with no charging plan or manual control involved, it takes the session over after ~8 minutes and pauses it. The five-minute SoC pulse on plug-in remains untouched; it is long over by the time Green would step in.

How Green lets go depends on the reason. On unplug or manual control, the clamp goes back to 16 A (otherwise a manually started session would stay stuck at 6 A), but as long as a car is connected that Green itself paused, the charge point stays exactly as it is: no uncontrolled release at the end of the day, so no car quietly topping up on grid power in the evening. When the nightly charging plan takes over, the charge point gets its three phases back if Green had it on one. All thresholds and wait times are adjustable via Settings ("Zon-laden — regelparameters"), with a sun-position readback so you can see immediately whether the entered location is correct.

The measurement series runs seamlessly across the migration thanks to the logical/physical device model. A dedicated log collector fetches the charge point's log file once an hour (with a response-time canary that flags memory pressure early), so charge point restarts, web server congestion and load-balancing behavior can be investigated from within Homey_Log itself. The line_id in that log file is not an incrementing counter but a position in a circular buffer that wraps every ~21.5 hours; the collector therefore deduplicates on ts (real wall-clock time, always increasing) with UNIQUE(ts, line_id), so a wrap never drops any lines. The restart detection only looks for words that never appear in normal operation: "power on" by itself is misleading, because that is also the name of the perfectly ordinary line the charge point logs when the charging contactor closes — every charging start would otherwise produce a "possible charge point restart" warning.

Two home batteries, multiple control strategies

The batteries can be set to a dynamic strategy that follows the quarter-hour plan, to a fixed balancing mode that simply regulates toward net-zero metering in near real time, or to standstill. Which of the two is active even shifts with the seasons: in summer the house runs continuously on balancing mode, in winter on the fine-grained quarter-hour plan.

Log source: the home batteries themselves

Following the model of the charge point log collector, Homey_Log also watches the batteries directly, through their local API. The Sessy has no log file like the Alfen, but it does have a rich state machine (14 system states, including fault and override states, plus strategy, firmware and P1 statuses); the collector polls the status API every minute and logs only the transitions — that is the event log. Readable in its own tab (Sources → Sessy log) and woven into the general Logs tab (origin "Sessy"; faults as warn/error, strategy and firmware events as info, routine switches as debug). The deeper events (strategy switches, firmware/OTA and the P1 dongle status, which on firmware v5.x only serves the v2 API anymore) start flowing once the sticker credentials are entered per device (Settings → "Thuisbatterijen (Sessy)"). The Sessy itself keeps no fault-log history — its web UI derives messages live from system_state_details — so Homey_Log's transition log is the only place where that history is built up. The collector performs read requests only and never touches the control side.

Log source: the inverters themselves

Besides the charge point and the batteries, Homey_Log can also watch the SolarEdge inverters directly, through the same cloud API as the solar backfill. Here too there is no ready-made fault log (/alerts and /changeLog return 403), so it is derived from telemetry. The inverter mode itself is useless as an event source — at night it also reads STARTING — so only the fault modes (ERROR/FAULT/SHUTTING_DOWN/THROTTLED/LOCKED_*) are logged. What else counts as a signal: daytime telemetry gaps (a gap is provably a fault), insulation resistance (groundFaultResistance, a safety signal), overheating, and the number of connected optimizers (a dropout means a panel fault). The latter and the firmware versions aren't in the five-minute telemetry but in the daily /inventory call. Hourly polling, a 150-minute lookback window (covers a missed run), call budget comfortably within the SolarEdge limit (~7 of the 300/day). Readable in its own tab (Sources → SolarEdge log) and woven into the general Logs tab (origin "SolarEdge"). The collector performs read requests only; the kill switch lives in Settings → "SolarEdge — omvormerlog".

Network devices: IP/MAC reference for troubleshooting

Marc also runs a network tool of his own that keeps track of IP addresses, MAC addresses, vendor, location and online status for every device on the LAN, and that tool exposes a dedicated JSON endpoint (/api/homey-log/devices) specifically for Homey_Log. The tool already logs a line itself when a new device appears or an IP changes (straight to /api/log) — Homey_Log does something different here: once a day it fetches the full list and stores it as a current snapshot (network_devices, upserted per MAC address), not an event log. This is a reference table like devices or mapping: viewable in its own tab (House → Network) and directly queryable with SQL by the support agent (schema-guide.md), so that during troubleshooting it is immediately clear which IP/MAC belongs to which device and whether it is online. Settings → "Netwerk-apparaten" holds the endpoint URL, the poll interval and a "Nu ophalen" button.

The charge point versus the battery: who wins? (XOM)

The moment the car starts charging, the batteries have to behave differently than usual — they can't just keep balancing along, because part of the car's charging power would then quietly come out of the battery. Instead of simply halting the batteries (the safe but wasteful option), during car charging they balance toward "zero + charging power": they cover only the non-car household consumption and thus can never drain into the car. The mechanism is a Homey script that sets an import target on the battery control every minute, fed back through the charge point. The motivation comes from the hindsight benchmark (see chapter 08): it shows the batteries structurally achieve only ~79% of their theoretical optimum — a gap of roughly €16/month — a good chunk of which comes from the batteries standing idle during car charging instead of working through.

This mechanism is not limited to "just the charge point": Settings has a configurable exclusion list — devices the batteries do not compensate for, with an adjustable threshold (from how many watts it counts) and window (nights only, always, or "fixed power"). That last mode is the most interesting one: it says "this device charges at a fixed power — plan or manual control — so count it in", and switches itself off at the moments when solar charging is controlling the charge point itself. The batteries therefore don't stand still during the day as soon as the car starts charging: they simply keep covering the house, around the clock. During solar charging they hold the house at zero and the solar surplus goes to the batteries and the boiler first, and only then to the car — which has the cheap night plan as its fallback. To keep the car from draining the battery, the solar controller looks straight through the battery: power coming out of the battery doesn't count as sun. A built-in safety clamp ensures the system never "leaves alone" more than what the house is actually drawing from the grid and the batteries at that moment — so with a solar surplus nothing happens, even if a device is on the list.

The Usage chart shows the same yellow solar layer with a green battery layer on top of it at every time scale — quarter-hour, day, week, month and year; the multi-year comparison (several years side by side) shows the solar yield as such a layer too. That band also makes the exclusion list visible: the excluded consumers are stacked at the top of the bar (with a dark border and a ⚡ mark in the legend), so above the solar/battery coverage band that sits behind the bars. At every scale you can thus see at a glance that exactly that consumption is not compensated by the battery — it sticks out above the colored band.

There is one more block in that same bar, just below the car and the boiler: the charging of the batteries themselves. That, too, costs energy, just like the car and the boiler. Leave it out and the space between the bar and the yellow solar layer looks like feed-in, while in reality power is flowing into the battery. Whether the battery is charging on sun or on grid power makes no difference to this block: it is simply consumption. The block deliberately sits just below the car and the boiler, not above them: the batteries only charge on solar surplus, so you expect that block to always stay within the yellow solar layer. The car and the boiler are allowed to stick out above it, since they are also fed straight from the grid. That makes it visible at a glance whether the picture holds — with one known exception: at night the battery sometimes tops up from the grid after all, in the cheapest hours, and then the block sticks out above an empty solar layer. The batteries feeding back is the green band that sits behind the bars; whatever yellow still sticks out above it is the real feed-in to the grid. That band has to be computed separately at each scale: add charging and discharging into a single sum and they simply cancel each other out at the day and month views, leaving a systematic zero.

Dishwasher planning

A script continuously recomputes the cheapest start time for the dishwasher based on the current quarter-hour prices, with three possible outcomes that each carry their own meaning: a fresh plan, "no dishwasher planned" (there is simply nothing waiting to run), and "existing plan is still current" (the script merely didn't recompute, which is not a synonym for "not planned").

What it looks for is the cheapest hour, and that follows from what the machine actually runs. The short forty-five-minute programme suits this household fine — it is faster, it comes out just as clean, and measured at the plug it uses less than the long "eco" programme, which contradicts the energy label but comes out of the house's own measurement series twice, independently. A machine that runs for three quarters of an hour should not be planned as if it were busy for four hours: a four-hour block contains the expensive hours next to it by definition, and the cheapest block is rarely the same as the cheapest hour.

Local first: talking to the device directly

A device in the house can almost always be addressed without a middleman. The route via a cloud service or via the home automation hub is a detour: slower, poorer in data, and dependent on two parties that can both fall over. Where it can, Homey_Log therefore talks straight to the device on the local network, and per device it is decided separately whether that local path takes over control or merely sits alongside it.

The vacuum robot is the clearest example. Where Homey gave one usable number — the battery percentage — the local connection delivers forty-five of them within seconds, including the number of missions run. The detail screen therefore shows the last runs with how they ended (docked, stopped, fault) and the days it did not go, with the reason; scheduling and starting happen locally here too. The curtains have a local mode with their own interface, and there the app is the only path that touches the motor — Homey still decides (evening routine, voice, a virtual device) but calls the app to do it. The dishwasher speaks an encrypted local protocol with a per-device key; there the app deliberately only listens in, in parallel with Homey, until there is a decision to start it locally as well. And the weather station's gateway delivers, in the very same call that fetches the soil moisture sensors every quarter-hour, the temperature, humidity and air pressure of the meter cupboard it hangs in: no extra request, no extra poller, and an air-pressure reading the detour promised but never filled.

Car telemetry

A separate integration delivers the actually measured battery level of both cars, plus charging and location status, rather than an estimate — so the mobile and desktop tiles show a percentage that matches what the car itself reports, not an indirect Homey guess.

08

The conductor: who gets the sun, and when

Everything in the previous chapter plans for itself. The charging plan carves up the night as if the car were the only consumer, the dishwasher hunts for its cheapest hour as if the battery didn't exist, the battery fills itself on the cheapest quarter-hours as if the water heater never asked for power. Four separate calculations, each convinced it is alone in the world — and the same kilowatt-hour promised to three appliances at once. Against that stands one place that answers the question in a single pass: who gets the solar surplus, who gets the cheap quarter-hours, and who waits?

Every minute the conductor fetches the solar budget, asks each participant what it needs, distributes, and records per participant what came out — always across a horizon of forty hours, in quarter-hours. It is deliberately not a joint optimisation but a waterfall down a ladder: a participant sees as free only what the rungs above it left behind. That order is a setting, not code — today the water heater's base need comes first, then the dishwasher, the home battery, the car, and only then the bonus portions for water heater and car. After that a small, fixed set of safety nets runs over the top: negative prices, comfort floors, phase assistance. Anyone with a hard end time — the dishwasher has to be done, the car has to drive — always makes their deadline, on grid power if need be.

Which appliances take part is not something the conductor knows by itself. Every controllable appliance registers itself from its own adapter file, under a small contract: how many kilowatt-hours do I need, by when, between which minimum and maximum power, in what minimum blocks, may I be interrupted, where is my comfort floor, and what is it worth to be finished early. The register itself knows no devices at all; an adapter that needs a physical device asks for it by its functional role. A mistake in such a registration — a duplicate key, a missing function — trips startup rather than only surfacing when the conductor happens to need that participant.

The day as a musical score

What comes out is visible as a planning strip: three rows of eight hours, four staves per row — one per participant, always in the same order — and thirty-two quarter-hour cells per stave. Left of the current quarter-hour is what actually happened, right of it is what is planned, and the current quarter-hour is a playhead running straight across all four staves. Colour carries the meaning (charging is not the same as discharging), and an appliance's letter sits once to the left of its stave rather than inside the cells. Beyond that there is nothing to tap: it is a score, not a control panel. The same strip appears on the phone, the wall tablet and the desktop, and labels, letters, legend and order all come from the server — the browser computes nothing here.

And it controls nothing yet. The main switch stands at shadow, every participant is in dry-run, and a round demonstrably writes not a single variable to Homey — not even an "off". The conductor computes, plans and records; the existing scripts keep doing the steering. That is not caution out of habit but the only way to answer the question that matters: is the conductor's plan genuinely better than what happens now?

That question has a yardstick of its own. Per appliance, the bill of the existing process is laid afterwards next to the bill of the shadow plan — the same amount of consumption, settled against the moment the shadow plan was made, with solar surplus counting as a free quarter-hour. An appliance may only start controlling for real once, across at least ten comparable days, it achieves a median cost difference no worse than the old process, produces no more missed requirements, and keeps a solar share within two percentage points of the old one. Median and not sum: otherwise a single outlier carries the whole verdict.

The water heater, as the first participant

Hot shower water is the most rewarding appliance to plan this way: the tank is a battery, only with water in it. The water-heater controller decides every minute anew whether the element belongs on solar, on the grid or at rest, along four rules in a fixed order of precedence. Solar is the default state: the element modulates along with what is being fed back, minus whatever the home battery is discharging at that moment — because a discharging battery is not sunshine. The floor beats everything: if the tank drops below 50 °C it is reheated regardless of price. The plan converts the showers expected before the next free or cheap window into kilowatt-hours and places those in the cheapest quarter-hours; in quarter-hours where a shower is expected the reserve is higher, because two showers back to back drop the tank by some 19 degrees and the mixing tap stops mixing somewhere around 45 °C. And the hygiene rule makes sure the tank reaches 60 °C at least once a week, in the cheapest quarter-hour of that day. If the location system says nobody has been home for more than a day, the floor and the plan lapse and solar and hygiene remain.

What makes that possible is a thermal model of the tank, and it is not there for show. The sensor sits at the bottom and lags for hours after a shower: the immediate drop measures around 3 degrees where in reality 10 to 12 left the tank. So an energy balance is kept at minute granularity — the standing loss, the tank's content per degree, the element's efficiency, and what every measured litre of tap water takes out. The sensor only counts as a reference point again after hours without drawing and without heating; with no reference point, the model simply says it doesn't trust itself.

On the phone there is one button: Nu verwarmen ("Heat now"). It sets an override that beats both the plan and the solar state — heating on the grid, up to a limit, for at most an hour, always cancellable. Pressing twice gives the same answer as pressing once.

An appliance that says nothing back

The water heater is controlled over radio and reports nothing back: one-way traffic. The only evidence that a command arrived is the smart plug in front of it. The requirement is therefore not "are you in the right state" but I want to see the transition: after a "grid" command the draw should be above 2,000 watts within three minutes, after "rest" below 50, and when falling back to solar without surplus the element should demonstrably switch off. If that evidence fails to appear, one retry follows, then a notification, an error line and a fallback to the safe solar state with a half-hour block.

One distinction is hard here, and it is exactly the pitfall that surfaced elsewhere in this story: a frozen measurement proves nothing. If the power sits above the threshold while that same value has stood unchanged for minutes, the system reports "the measurement has stalled" and not "the water heater isn't responding". Two different faults, two different fixes.

The dishwasher, on a shadow price

The cheapest electricity isn't always the cheapest electricity. Running on an afternoon with a solar surplus that would otherwise go onto the grid for a dime costs less than a night at a low price you still have to pay. The shadow price puts that on one scale: your own solar surplus counts as zero, the rest at the quarter-hour price of that moment.

Applied naively, that sum picks the earliest free moment, and that is precisely the wrong choice. Early in the morning the forecast surplus covers the cycle by a hair; midday it is four to six times as large. Both are called "free", but only the second stays free if the forecast disappoints by half an hour. The rule therefore first takes all start times within five cents of the cheapest, and among those picks the earliest at which the expected surplus covers the cycle at least three times over; failing that, the moment with the most slack. Across a look-back of twenty-three nights the average price per cycle drops from nearly thirty cents to nine, with well over a third of the cycles genuinely free — although that was measured in the sunniest month of the year, and in the dark months the solar step does almost nothing.

This planner too runs in shadow: it computes, records and starts nothing. Who does the planning is a single switch — the starter keeps a handover flag in Homey equal to its own state and does not start if that handover can't be confirmed, so there are never two planners at the same machine.

09

Burden of proof: is it actually right?

A system that grades itself needs an independent yardstick — otherwise it's just checking its own homework. So alongside the AI layer sits a deterministic validation stack:

  • Hindsight benchmark: for every day, an exact dynamic-programming optimum is computed after the fact: what the best possible battery deployment would have earned, given that day's actual solar output and actual household consumption. The gap between that optimum and the real outcome is the hard yardstick for "how much is left on the table" — the benchmark shows a battery utilization of ~79%, a gap of roughly €16 a month, and was the direct trigger for the battery-during-car-charging mechanism described above.
  • Golden days: a frozen set of eight checks against the real API endpoints, deliberately re-frozen after every invasive data-model change — a regression test for the numbers themselves.
  • External reconciliation: monthly and daily figures are laid alongside the energy supplier's (Tibber) as an independent second source. That comparison, incidentally, exposed a structural undercount of live import consumption during certain transition hours — a finding that without the external check would in all likelihood never have surfaced.
  • Consistency watchdog: a daily background check that re-derives random days from the quarter-hour source and compares them with the stored daily value, and monthly against yearly totals — purely to prevent two tabs from ever silently drifting apart.
  • Invariants: twelve statements that should always be true — the daily total is the sum of its quarter-hours, solar output is never negative, the water meter never runs backward, the battery can't deliver more than physics allows, the schema in the database matches the one in git. Every night around four o'clock, that list is run against the fresh day. Each statement records not just its margin but why that margin exists; a tolerance without a reason is, by agreement, a design flaw. An invariant that doesn't apply here — no data, a paused measurement point — reports itself as "skipped" rather than "passed", because those are two very different things.
  • Night watch: at a quarter past five, the server runs its own full test suite plus a smoke test through the screens — against the code that is actually deployed at that moment, not whatever sits in a working directory. The timing is deliberately a different part of the day than when development happens: a test that secretly depends on the clock was green in the evening and betrays itself here. If it fails three nights in a row, a phone notification follows.
  • Disaster drills: monitoring that has never been exercised is an assumption. So an outage can be simulated on purpose — making the weather stations unreachable, dropping the electricity price, silencing the energy ingest — after which a scorecard shows what the system actually noticed on its own and whether the missing data came back by itself. Every drill has an end time that expires even without intervention, so a forgotten drill can't turn into a real outage. The scorecard deliberately counts only alerts a human actually gets to see: a warning that lands only in the deepest log lines counts as missed.
  • VM usage monitor: a nightly read-only check of the server itself — disk usage, database size and row counts go into a system_metrics table as a time series and into the log as a line, with a growth chart under the "VM Usage" tab (in the "Homey_Log" menu). Configurable thresholds send a Pushover alert before the disk fills up, so data growth becomes visible before it becomes a problem.
  • Green telemetry ("Solar charging"): a standalone server-side sampler that, alongside Green's own decision logic, records the reality of solar charging — charger-applied current per phase, P1 phase currents and load, Sessy power/SOC — in a wide, raw green_telemetry table. Adaptive cadence: every 5 seconds as soon as the car is plugged in, otherwise every 30 seconds, so every correction Green makes is captured in high resolution without needing a separate "burst around the setpoint". The tab (Energy → Solar charging) shows a uPlot chart with setpoint command vs. charger-applied and Green_Tick events as timeline markers, plus a table with CSV export. The chart also puts the phase currents of house and charger side by side: color says which phase (light, medium, dark blue for L1, L2, L3), line style says which device — dotted is the house, solid is the charger. A dark vertical line marks every moment control switches between solar charging and the night plan. Deliberately without automatic analysis or verdicts: the table is the raw measurement source, interpretation happens in the reviews.
Fresh or frozen? — the measurement layer beneath every sensor

A recurring pattern in this house: when a device stops reporting, nobody notices automatically. Homey simply keeps repeating the last known value as if nothing were wrong — available cheerfully stays true. A device that has gone silent therefore doesn't look like a failure; it looks like a device that happens to be using nothing.

That has gone wrong three times, each one discovered by accident rather than by an alert. The most expensive case: the solar inverter in the garage sat idle for five days while the measurement kept dutifully reporting 0.00 kWh, and 109 kWh of solar yield vanished from the books — found only because the energy balance happened to be recalculated for an entirely unrelated matter.

Every quarter-hour measurement therefore records not just a value, but also when the device last genuinely refreshed that value. This is visible under Settings → Data & system, in the "Measurement gaps" card, right below Retention. A few measurement points — such as the battery load, which comes from a calculation rather than straight from a device — structurally can't carry such a freshness stamp; they're listed separately as "no freshness evidence" and don't count as misses. The card names each measurement point by its everyday device name ("Outdoor lights"), with the technical key in small print behind it, and with a choice of period: the last 14, 30 or 60 days.

Important: that card measures, but blocks nothing on grounds of age alone. That's not unfinished work but a measured conclusion. An automatic rule that would reject measurements once their stamp grew too old was built, run in observation mode without blocking anything, and rejected on its own numbers: it would mostly have refused valid measurements and caught not a single real freeze. The rule was then removed rather than kept dormant — code that does nothing but looks like monitoring is more dangerous than no monitoring.

The reason such a rule can't work lies in the stamp itself: it says when a value last changed, not when a device last made itself heard. A device that's switched off stays at zero and thus never gets a new stamp. The evidence is airtight: for the tumble dryer and the electric auxiliary heater, the stamp coincides to the minute with their last quarter-hour of actual use. "Frozen" and "off" are simply indistinguishable. That also explains the false alarms the card opened with: six wall sockets that seemed to have been dead for months turned out, one by one, to be devices that are simply switched off — the Christmas lights live in a box, the outdoor lights are seasonal work. What remains is an observer with a human behind it, and here that's the honest outcome.

Deliberately silent: the pause agenda. A device that's intentionally off shouldn't keep complaining. Every measurement point can therefore be paused — with or without an end date, and with a reason attached. The outdoor lights and the Christmas gear are paused permanently, the washing machine during a vacation. One switch pauses a whole set at once (vacation mode) and reverses it automatically on switch-off, or at the end date; pauses Marc set by hand himself are left untouched. Checking a box pauses immediately, unchecking resumes immediately, even on a mode already running — a checkbox that only takes effect next time is a checkbox that lies.

One distinction is hard and fast: pausing suppresses the alerts, not the measurements. Measuring and storing continue unabated. If measuring stopped too, nobody would ever see that the device works again — and then a pause isn't rest but a blind spot.

One thing does block something. When the garage inverter started measuring again after its outage, the lost solar yield was restored twice from the inverter manufacturer's data — and twice Homey subsequently reset those same quarter-hours to zero. The explanation turned out to be measurable: Homey repeats its own measurements for exactly 24 hours, so every repair within that window was systematically flattened. A restored solar value can no longer be wiped by a zero once the quarter-hour is more than two hours in the past; fresh quarter-hours may still correct themselves. Such a refusal shows up as a warning in the log, because a silent correction is exactly what you don't want here.

The lesson that has stuck from this whole stack: internal consistency checks don't see the kind of error that kept surfacing here — only verification against an external, independent source (the energy supplier, the source table, the actual Homey flow) exposes it.

10

How this gets built

The whole project is under git version control. The way of working is itself a small showcase of this article's subject: one AI model writes a specification and reviews afterwards, a second model carries out the implementation, and every non-trivial change is verified live against the real Homey before it counts as "done" — including, in the experiments phase, a deliberate test with a genuinely triggered emergency stop, not just a simulation.

Three layers, and one test. The code knows three layers that may not reach into one another. At the bottom the data: tables of measurements, exactly as they arrived. Above that the mapping: adapters, registers and roles — there, and only there, does it say which device is the charge point, which series is called "solar" and which sensor measures the water heater. And on top the screens, which know functional concepts only: no device identifiers, no device name as a selection mechanism, no formula in the browser. The test fits in one sentence: swapping out a source must break nothing above the mapping. Replacing a broken sensor ought to be one line in the mapping, not a hunt through the screens.

How much evidence a change has to produce depends on what it can break, not on the folder the file sits in. Nothing touches the storage layer — schema, migrations, data pruning — without tests up front, a backup and a written-out rollback path; a mistake there is irreversible. The same goes for the calculation model, because an error there contaminates years of history and every proposal built on top of it. Processes that send anything back to Homey carry the requirement of an emergency brake and verification against real behavior: those errors end up in the physical world, as a car that doesn't charge. Only for pure display — a color, a piece of text, a different layout — is it enough to check that it looks right in the running app. The boundary keeps itself clean: the moment a screen computes a new number, that calculation belongs not in the browser but on the server, with tests.

One building block per concern — and a test that works out for itself who is bypassing it. Every cross-cutting concern has exactly one: writing a log line, computing in Amsterdam time (daylight saving included), rounding to quarter-hours, reading a config file, fetching a secret, polling periodically, reporting that a source has dropped out, starting a process, registering a table, choosing a retention period. What makes that enforceable is the shape of the accompanying test: it derives from the code itself who ought to be using the building block, instead of ticking off a hand-maintained list. A detour therefore surfaces on its own; every exception has to carry a reason; and an exception nobody needs anymore turns the test red rather than quietly riding along. Twenty-six of the test files are of that kind.

Loading does nothing. A module that gets loaded touches no database, no file, no network and no timer. The database is a lazy façade: the connection comes into being at the first genuine database action, and only then do the schemas the modules registered actually run. That isn't tidiness for its own sake — as long as loading itself created tables and opened connections, every little helper script joined in unnoticed. In the same spirit, startup is a register of declarations instead of a row of loose calls: each process states how it starts, whether it is required, what it queues behind, and which collectors it feeds. If it is required, a failure stops startup. If it isn't, the server simply comes up, the failure goes into the logbook, and the matching collector is marked "did not start". That last part is the point: a collector that fell silent because of a startup failure was otherwise indistinguishable from a collector that rightly has nothing to report.

Underneath lies a safety net of nearly seven hundred test files. On every commit the always-set runs — all contract tests, all role tests — plus precisely those tests that claim a changed file as theirs: ten to thirty seconds. If the commit touches the server itself, the database, the dependencies, or more than twenty-five files, the whole suite runs after all. The trade-off is deliberate: a test that doesn't claim the changed file only turns red at night. Two gaps are baked in by construction — the check looks at the working directory rather than at what is actually being committed, and git skips it when branches are merged. After a merge the full suite therefore runs anyway, loudly but without being able to block, and the night watch on the code that is genuinely live remains the conclusive proof. And if anything changes in the database structure, a fresh snapshot of that schema has to go into the commit; forget it, and you'll hear about it from the nightly invariant that lays git and database side by side.

What's left of the mistakes

Outside this repository sits a central layer of lessons: short, hard rules drawn from mistakes actually made, spanning projects, with a cap on their length and no way to delete one. At the start of substantial work they are fetched and followed, through a read client that by construction can only read and that always puts the newest on top — a lesson you don't read is the same as a lesson that isn't there. A lesson can expire, and a lesson can be reaffirmed.

The same place carries a mailbox between projects. If work here gets stuck on something only another project can answer, that question simply waits there until it's picked up: nobody sits waiting, and a budget per topic keeps a mailbox from turning into a chat window.

11

The family dashboard: a third entrance, for the whole household

Alongside the desktop admin environment there is /dashboard/, built as a permanent wall tablet for the whole family — including de tweede auto, een gezinslid and een gezinslid, not just Marc. On the wall tablet it runs as a kiosk: dark by default (evening use on a wall), big tiles and numbers legible from about two meters away, no navigation to admin, logs or experiments, and a timer that automatically returns to the overview after a minute of inactivity. 📱 That same dashboard also has a phone view: /m redirects to /dashboard/?view=phone, and it's that URL — not the screen width — that determines how it looks: full-width tiles stacked vertically, the calendar hidden, popups as a bottom sheet instead of a floating window. Screen-width detection was deliberately avoided: it's unreliable on a real phone, presumably because of stubbornly cached CSS. /dashboard/ without a parameter — the wall tablet — is always the tablet view, even on a phone-sized screen. For everyday phone use there's also the dedicated app (the next chapter); this view sticks around as a direct look at exactly the same screen that hangs on the wall. 🤖 Deliberately one component instead of two: a tile that changes on the wall tablet changes along with it automatically. Admin work (proposals, logs, chat) doesn't belong there; that happens on the regular desktop site, which supports direct tab links (/#voorstellen). The same rule applies here: purely a presentation layer, no new data source, just reuse of existing /api/... endpoints and the existing write whitelist for lights.

Both views share a single stylesheet, but not a single set of dimensions. Every size is expressed as clamp(minimum, breedte-afhankelijk, maximum), and on a 390-pixel-wide phone screen that middle term is so small that everything would land on the minimum — the smallest variant the design knows. So the phone view sets its own sizes: reading 58px, label 20px, tiles 220px tall, with the top bar (title, clock, status pill, faces) and the popup content (rows, lists, buttons, chart labels) scaled in proportion. The wall tablet sits at the top of those same ranges — 36px, 152px-tall tiles — and stays there, because all the phone sizes hang off the view switch and never apply to the tablet. The energy-flows diagram deliberately keeps its own sizing: it's a drawing with fixed geometry, where larger labels would run over the circles.

For a home-screen shortcut the dashboard ships an apple-touch-icon and a manifest — without those two, iOS just takes a screenshot of the page as the icon; an SVG favicon isn't used for that. There are two of them: the phone gets a manifest that launches in phone view, the wall tablet one that launches in tablet view, so a saved app never opens in the wrong view. The head script picks the right one and creates that link itself, rather than correcting a fixed link after the fact: Safari fetches the manifest during parsing and would grab the wrong one.

Main screen of the family dashboard: twelve tiles (energy, home batteries, both cars, lights, hot-water boiler, dishwasher, robot vacuum and the energy KPIs) on the left, the family calendar on the right
The main screen: twelve tiles in three rows with the family calendar on the right (blurred here), the presence row (who's home) at the top, and the quick actions at the bottom. The glow/pill follows the house status.
TileData source
Energy nowthe last complete quarter-hour from energy_quarterly (grid_import/export, solar) — no live wattage reading available, so explicitly labeled with "updated X min ago"
Home batteriesaverage sessy_soc_1/2, charge/discharge direction derived from the difference with the previous quarter-hour
Cars (2×)the same /api/car-status endpoint as the desktop — dynamic, no hardcoded number of cars; also live charge-point telemetry (whether, how fast and how a car is charging)
Lights & moodsdevices table (class light) × live onoff status; can be switched off per light or in bulk via the existing write whitelist. Second tab: Homey's Moods ("Sferen")
Boilerlarge in the center: water temperature, a live reading from the Shelly on the hot-water boiler ('Boiler Control', zone Zolder); at the bottom: current power draw from the boiler meter ('Plug-Boiler', via the logical-device layer the successor to the 'Douche' plug); shower indicator from the 'Presence douche' motion sensor. Popup with two tabs: Verloop (day profile: today's water temperature as a line with the measured liters per shower as small bars beneath it, plus the daily figure in kWh and €) and Douche-verbruik (shower usage: the measured liters from the pulse counter on the hot-water pipe)
Dishwashercar-status.js's buildDishwasherStatus() + live Bosch telemetry; start time/time remaining shown on the tile itself, plus a "Start nu" (start now) button
Robot vacuumlogic variables 'Stofzuiger Actief ?'/'Stofzuiger Geforceerd Gestart ?' (the ground truth of the Homey flows themselves) + live automatic schedule from the 'Stofzuigrobot starten' flow
Self-sufficiency / Direct solar / Consumption in EUR / Actual coststhe same compute functions as the Usage tab (computeZelfvoorziening/computeDirectSolar/buildEnergyAI), window always "today"

The tile set is deliberately kept narrow (a fixed, dynamically categorized device list rather than a full discovery UI) and shows no misleading tiles — this house has no real window or door sensors, for instance, so that tile simply doesn't exist. Every write action goes through the existing whitelist. The only global quick action is "alle lampen uit" (all lights off, with confirmation); beyond that, every individual tile button triggers an existing, whitelisted Homey flow — start the dishwasher, start the vacuum or send it to its dock, start manual charging, set a mood. Heavier quick actions are deliberately absent: only what can go through a whitelisted write path makes it onto the screen.

Behind the Energy tile sits an energy-flows diagram (grid/solar/battery/both cars → house, with a "nu"/"vandaag" (now/today) toggle and a kWh/€ switch, brought together in one calm control strip). The house is the breathing heart at the center — a soft glow in the status color, the same language as the breathing glow of the dashboard itself — and energy flows as light (particles) through curved conduits: inward on import, solar or discharge, outward on charging. Active nodes get a color halo, inactive ones stay quiet, so you can see at a glance what's alive. The flow split builds on routes/energy-model.js — the same module that feeds the Self-sufficiency/Direct-solar popups — and respects the balance identity net-in + zon + batterij-uit = huis + auto + net-uit + batterij-in without anything being tacked on separately. In € mode, every flow is valued at that quarter-hour's grid price (a deliberate simplification: no separate feed-in or charge-point tariff, unlike /api/effect-split, which does know those). The popup refreshes itself every 20 seconds while it's open; truly live isn't possible, since the underlying meter data only refreshes once per quarter-hour.

A second tab, "Verloop" (day profile), shows an hourly chart of today — the same areas reading as the Usage tab, in the calm kiosk variant: one consumption bar per hour with the solar yield behind it (yellow) and the battery supply (light green, stacked on the solar), plus the import as a dotted line. If a bar falls within the colored band, that hour was covered by solar/battery; whatever sticks out above it came from the grid. The source is /api/verbruik-data (hourly granularity), deliberately hand-drawn in SVG rather than through a chart library, so the dashboard keeps its own style and stays dependency-free.

Energy-flows diagram: the house as a glowing heart at the center, with grid, solar, home battery and both cars as nodes around it and energy flowing as light through the conduits
The energy-flows diagram in "Nu" (now) mode: the house is the breathing heart and energy flows as light through the conduits — inward on import/solar, outward on charging. Here the solar array is delivering 6.3 kW; the surplus goes partly into the battery (charging, 78%) and partly back to the grid (1.7 kW), while Marc's car charges on solar. Every node is also clickable for a day profile (more on that below).

The presence row at the top right of the header has one ground-truth pitfall: the "Smart Presence" Homey app uses exactly the same presence capability for family members as for generic network devices, so "Internetverbinding" (internet connection) or a freezer in the shed report themselves as "present" just as readily as Marc or een gezinslid. A name allowlist (just like the car identifiers in car-status.js) filters that out. Every family member has their own photo in the row; anyone without one falls back to a circle with initials in their own color. Those photos are deliberately kept small — a portrait straight out of a phone camera easily weighs more than the entire rest of the app, and someone on a mobile connection pays for that every time they open it.

The dishwasher is a Bosch Home Connect appliance with real status telemetry (progress, time remaining), with one quirk: when idle it reports an absurdly high "time remaining" (241 hours) as a sentinel for "no active program", so the tile treats anything above 5 hours as "not active". The detail popups of the home batteries (per battery) and both cars show a day-profile chart (00:00 → now): battery history from energy_quarterly (the same sessy_soc_1/2 as the tile), car history from the Tronity_Publish_Vehicles log entries (a sample roughly every 2 minutes). It's a hand-drawn SVG line chart with a color pair that passed the dataviz validator (colorblind separation, contrast on the dark surface).

Car popup with a state-of-charge line chart of today, from midnight to now
The car popup with the day profile at the bottom: state of charge since midnight, including the charging session that lifts the percentage in one go.

A fullscreen camera popup appears automatically the moment someone rings the doorbell. Homey has Ring fully integrated (7 cameras + 1 doorbell), so the camera-snapshot mechanism of the 3D view in the House tab is directly reusable for the showing; for the "automatically at the moment of the ring" part, a small Homey flow (Deurbel_Gebeld) logs the ring via the generic postlog route. A true video stream isn't possible — Homey's camera-video and WebRTC layer are producer-only and not exposed to external API clients — so the popup refreshes a single snapshot every one and a half seconds. The "action needed" status pill only counts a genuine error; the pill and the notifications status go dark after opening the notifications popup until something new arrives after that moment ("seen" status kept client-side in localStorage, the server stays stateless).

Every node in the energy-flows diagram (grid, solar, home batteries, and each car separately) is clickable and opens a bar chart of today (per hour since midnight) that stacks on top of the open energy-flows popup without closing it. Grid and batteries get a two-direction bar chart (import/feed-in, charging/discharging); solar and the cars a single one. There's only one charge-point meter, so attributing which kWh belongs to which car uses the Alfen_Resolve_Active_Car log entries: for each quarter-hour, it checks which car was active. In the diagram every car has its own node (Marc's Tiguan and de tweede auto's A3, each with its own charging power), attributed to whoever is actually charging at that moment.

Bar-chart drill-down: solar generation per hour since midnight, stacked on top of the energy-flows popup
Click the solar node → a bar chart of today, stacked on top of the energy-flows popup (visible in the background) without closing it.

The lights are grouped by room (the room name from the devices table becomes a section header). The lights tile has a second tab with Homey's Moods ("Sferen"), each with a fitting icon. Homey's Moods manager doesn't exist in the local Web API this app uses, so each mood runs through a minimal 1-to-1 pass-through flow (Dashboard_Sfeer_<naam>), whitelisted in TRIGGERABLE_FLOWS — the same pattern as the other flow buttons. Homey has no "which mood is active now" state (a mood is a momentary action), so the moods list deliberately shows no highlight. The 3D house can be called up fullscreen from the dashboard as an alternative control entrance: no separate rendering, but the existing House tab via an "embed mode" (?embed=huis) in a fullscreen iframe.

The boiler tile shows the water temperature large — a real measurement from the Shelly on the hot-water boiler ("Boiler Control", zone Zolder) on capability measure_temperature.1, passed through live to the big number. Below it sits the current power draw from the boiler meter, and a 🚿 indicator for "someone is showering", derived from the motion sensor. The popup has two tabs. Verloop lays out today's water temperature as a line with the measured liters per shower as small bars in the same picture: that way you can see at a glance that a shower punches a dip into the boiler, something two separate charts side by side can't show. The power consumption is deliberately not in there as a third series — the daily figure in kWh and € sits as a line below the chart, so the number is there, but it doesn't clutter the picture. Douche-verbruik shows the measured liters from the pulse counter on the hot-water pipe. Keeping those two apart is intentional: an estimate from heating moments and a real water measurement look alike and are not, and precisely that confusion was the reason to state explicitly where each number comes from.

The home-batteries popup shows each battery's monthly charge/discharge efficiency (ηcum) — the same green percentage as the Battery-η tab, not state of charge. The chart sits directly below the combined percentage (first the shape of the day, then the details), with the level and the efficiency per battery side by side in two columns.

The car tile shows not just the SOC, but also whether, how fast and how a car is charging. The "how" (charging plan / solar charging / manual) can't come from the charging plan alone — a car that's charging while the plan already reports "target reached" is a manual override — so car-status.js also reads the charge-point device (evcharger_charging + measure_power) and reasons: a valid plan reason wins, otherwise "manual" is the explanation. A "start manual charging" button in the car popup appears the moment that car is plugged into the charger.

Whether a car is plugged in isn't left to Tronity's word alone. Tronity provides a "plugged in" flag per car, but it can stay stuck for days when the car is asleep: de tweede auto's car once sat for two days on "plugged in and full" while the charge point was empty the whole time. The charge point is the only one that physically knows — but there's one charge point and two cars, so it doesn't know which car is plugged in. Hence the rule: the charge point may only contradict. If it reports nothing is connected, then no car is plugged in; if it reports something is connected, Tronity decides which one. If the charge point briefly can't be read, Tronity stays in charge — a network hiccup must not hide an ongoing charging session. Current through the cable counts as proof that something is connected, even if the state field claims otherwise; power doesn't lie.

Which charge-point device gets consulted for this deliberately doesn't go by name. During a migration the old name lives on while the device behind it delivers nothing anymore — and that's the quietest failure mode there is: the charging power and "charging now" sat empty for weeks without a single error. No error came, nothing came. The choice therefore falls on the charge-point device that actually returns a state, so that the next device swap heals itself. A robot-vacuum tile (Roomba) reads its automatic start time live from the Homey flow Stofzuigrobot starten itself (no hardcoded time); whether the robot is running right now comes from the same two logic variables that Homey's own vacuum flows use as ground truth, so the tile always stays in sync with what the automation thinks.

A family calendar (Google Calendar embed) is permanently on screen, not behind a tile. The main screen is a two-column layout: the tile rows with the quick actions on the left, the calendar card at full height on the right; tapping it opens a fullscreen popup that can be scrolled. Since it's a cross-origin Google iframe, the Google chrome can be stripped away (via the show*=0 embed parameters) but the internal layout can't — Google renders that itself and follows the device's dark mode. A transparent "click catcher" layer above the iframe catches the tap (the iframe itself would swallow clicks); the card refreshes every 30 minutes with a cache bust so "today" stays correct on a tablet that never turns off.

The presence avatars are clickable: a popup per person with home/away, "last seen" and a "Vandaag" (today) list of arrived/departed moments since midnight. That timeline comes from device_events: the generic "device events sampling" flow sends a fixed list of capabilities from every device to the server every minute, and the server itself decides what counts as an event — presence is in EVENT_CAPS, the same path as lights on/off. (Smart Presence attaches presence to non-human devices too; those are filtered out at display time, not in the generic script itself.)

As a kiosk the dashboard refreshes itself every 5 minutes (a wall tablet doesn't need more), with a manual refresh button in the top bar and an "updated X ago" label that ticks along every 15 seconds — even when the refresh stalls. If the label visibly ages, you know something is off, instead of staring at a frozen but current-looking screen.

Where "today" begins. The server itself runs on UTC, and in summer that's two hours ahead of Amsterdam midnight. Every day window on this dashboard — the energy-flows diagram, the hourly "Verloop" chart, the day-profile charts of cars, batteries and boiler, the "Vandaag" presence list and the self-sufficiency figures — therefore explicitly calculates with Amsterdam midnight, daylight saving time included. Without that conversion, the first half of the night is systematically missing, and that only betrays itself when you happen to look: a shower just before two a.m. that simply wasn't in that day's chart.

12

The phone app: the house in your pocket

The wall tablet hangs on the wall and the admin environment lives on a big screen. What was missing in between is the question you ask while you're out: is the house still doing its thing? For that, there is a dedicated app on the phone, simply named Thuis ("Home"). It's a web app that behaves like a real one: you put it on your home screen, it launches in its own window without an address bar, and it opens even when you have no connection.

Here too you sign in once with your own account, and your role determines what you see: anyone with view-only access never gets control buttons put in front of them. The app has four screens. Nu ("Now") is the state of the house in tiles — energy and the energy conductor, the home batteries, both cars, today's solar, self-sufficiency, the climate, the garden, the boiler, the heat pump, the dishwasher, the lights, the curtains, the robot vacuum, the waste collection calendar, the daily report, the notifications, where the family members are, and which room you are in yourself. Inbox holds the analyst's proposals and observations. Grafiek ("Chart") is the energy timeline with a period picker. Chat is a conversation with the same analyst who wrote the morning report — for anyone who has that conversation enabled. The administrator sees a fifth screen on top of that, Beheer ("Admin"): the same settings as on the desktop, but held in your hand. You swipe horizontally between the screens; the bar at the bottom does the same with a tap.

Behind every tile sits a full detail screen, and those screens are stacked like a deck of cards: you swipe from one to the next without having to return to the overview. The deck pre-renders its two neighbors, so a swipe never lands on an empty screen. You can control things too — start charging manually, switch on the dishwasher or the vacuum, turn off the lights, give the water heater an hour on the grid — always through exactly the same secured route and the same allowlist as on the wall tablet, with a confirmation prompt in front of it.

One house rule holds on the phone without exception: a chart underneath a tile is about today. If some other time dimension is shown — the last 24 hours, a week — it says so literally. Underneath every chart sits one line naming what happens along the horizontal axis, because a bar without an axis is a picture everyone invents their own story about.

The Nu screen of the phone app: attention items, presence, the day's progression and tiles for energy, batteries, cars, solar and the boiler Detail screen of the home batteries: charge percentage and today's progression, with the deck's stack indicator at the top The Grafiek screen: consumption and solar per period, with the period picker at the top
Three of the screens: Nu with the tiles and, top right, who is signed in; a detail from the deck (the home batteries, with the stack's row of dots at the top); and Grafiek with the period picker. Faces and names of family members have been blurred here.
What separates an app from a website

Three things make the difference, and none of them is cosmetic.

It opens offline. A small background layer stores the app's shell — the styling, the code, the fonts — plus a handful of data the Nu screen needs to show something. Only those few read routes are on an allowlist; anything that writes bypasses it entirely, because a cached response to "turn off the lights" is a disaster waiting to happen. Anything served from storage instead of fresh from the server is marked as such: better a visibly stale number than a fresh-looking number that is actually from yesterday.

It can reach you. Notifications arrive directly on the phone, even with the app closed, and the number of pending items shows as a badge on the app icon. Tapping one takes you to the notification itself, not to the home screen. A separate kill switch can silence the entire channel with a single line without touching the rest of the system. That channel deliberately sends for real, rather than spending a while merely logging what it would have sent. Shadow-logging simply cannot measure the failure mode of notifications — if a bug silences the channel completely, nothing appears in the log, and nothing looks exactly like a quiet house. The hard alarms additionally run over the Pushover channel, so an alarm without a push notification is a measurable miss.

Everything the house has to report goes through one door. The kinds live in a register — faults, a scheduled charging slot, the dishwasher, the daily report, the garden, the vacuum robot, a comfort question — each with its own explanatory sentence, a minimum role, and two separate switches: one for the notification in the app and one for Pushover. Pushover is therefore extra and never a replacement: an in-app notification is never suppressed because the phone alert is on, and Pushover is a house channel rather than a personal one — anything addressed to one person, such as a chat reply, doesn't go over it. Every notification states which watchdog it came from; a source that already has its own watchdog isn't reported generically on top of that, so no contentless echo sits next to the real message.

And it doesn't repeat itself. A notification that keeps arriving is not ten notifications but one problem. If exactly the same notification returns within the hour, a counter grows on the row that is already there instead of a new row and a new delivery appearing: "94× since last night, last one three minutes ago". Two kinds deliberately do not damp — the test button, because pressing twice ought to arrive twice, and the chat reply, because two identical answers are two answers. The trigger was measurable: in one follow-up measurement 94 of 130 notifications were the same warning, every six minutes.

It doesn't eat your data plan. An app you open ten times a day cannot afford data usage. The biggest cost there is the family members' photos: unprocessed and re-fetched every time, they added up to almost eight megabytes per app launch. So they have been shrunk fifteenfold and are refreshed at most once a day. You can't see something like that by looking at an app — you only see it if you explicitly measure for it.

The Now screen belongs to you, not to the app. Which tiles it holds and in what order is each family member's own choice: My settings lists them all, each with a handle to drag it and a switch to leave it out. That choice hangs on the account rather than on the device, so it travels along to a new phone. Which tiles are on offer lives on the server, not in the browser — when a tile is added it simply appears in everyone's list, with nothing to update. The same screen holds your own profile picture, and the switches that say per kind of notification whether you want it: anyone who doesn't care about a scheduled dishwasher stops getting that message, while the hard alarms keep reaching everyone.

Talking to the house

The conversation with the analyst can also happen out loud. The phone records, the server transcribes, the analyst answers, and the phone reads that answer aloud. Both the listening and the speaking happen locally: alongside the app runs a small process that keeps a speech model and a Dutch voice warm, because loading them cold costs a second and a half per turn. That process listens only to its own machine, the recording goes to a temporary file that is cleaned up no matter what, and the cloud fallback is switched off: if the local path drops out, you get a clean notification and nothing leaves the house. A counter per path makes it visible if anything ever does slip outward.

Recognition is given this house's names — family members, appliances, brands — and behind it sits a post-correction on exactly those proper nouns: predictable and testable rather than phonetic guesswork. And because transcribing costs roughly a third of the recording's length, the recording is bounded, and one that is too long is refused before the model starts on it. That saves a fault notification that isn't a fault.

The most important guardrail is about silence. A speech model that hears nothing invents a sentence — measured: one second of silence produced a fully fabricated question, compute time included. So there are two independent guardrails: the model never even hears the silence, and on top of that, too little genuine speech — or one of the known invented phrases — is treated exactly like an empty recording. "I didn't catch that", never a fabrication walking into the conversation as a question. One engine must never be the only guard against its own inventions.

The spoken answer is deliberately short: a few sentences at most, numbers written out, and the full text collapsed under "Details". If the model supplies no read-aloud version itself, the server makes one — so there is always something to read out. And there is a practice mode that does transcribe but does not send: the text lands in the input field and you press send yourself.

The boundary with the wall tablet is deliberate: everything the phone shows also exists in the main app. What the phone does differently is scale — readable in the hand rather than from two meters away — and order, because on the road you first want to know whether anything is wrong, and only then how the sun did today.

13

The waste collection calendar — straight from the municipality, on the wall tablet

The wall tablet already shows plenty: energy, household appliances, the family's calendars. Now added to that: the next collection day for household waste — big and clear, in the spot previously occupied by an "All lights off" button (that function also lives inside the lights tile itself). Every day of the year is listed in a municipal waste collection calendar; Homey_Log fetches it directly as an iCalendar feed (.ics), parses it and keeps track of every collection date of the year.

On the collection day itself, a notification fires at 6:00 in the morning — say, "GFT vandaag" (GFT today; GFT is organic waste) — and you can't just click it away. The notification disappears only when you explicitly press "Ik heb hem aan de weg gezet" ("I've put it out by the road"). Not by clicking next to it, not by swiping the window away, not on its own. And that deliberate moment of confirmation applies to the whole house at once: press the confirmation on the wall tablet, and the notification vanishes from your phone as well. No two separate notifications on two separate devices, each of which you have to dismiss on its own.

Put simply: the municipality supplies the data, Homey_Log makes sure the house knows when what gets collected, and the wall tablet — hanging on the dining room wall — helps make sure the trash actually makes it to the curb. No more, no less.

The existing Homey app Trash Checker, which shows the notice on the LaMetric clock in the kitchen, keeps running as before. Homey_Log doesn't touch that app — it's a parallel system, not a replacement. So Marc and the family get the same message in two places, and both work independently.

Why straight from the municipality?

Many waste collection calendars are offered by local municipalities as a web form: you enter your postal code and house number, and the municipality tells you which types of waste get collected on which days, by which route. That web form returns an iCalendar link. Homey_Log requests that feed — once at startup and then at four fixed times a day (5:00, 11:00, 17:00 and 23:00) — so there's no lag and no separate Homey app or integration is needed. If the municipality ever changes the route (which happens regularly), that shows up automatically as soon as the feed refreshes.

The four types of waste collected here are: GFT, organic waste (weekly); PMD, that is plastic, metal and drink cartons packaging (biweekly); papier, paper (monthly); and restafval, residual waste (monthly). That comes to well over a hundred collection dates a year — not a single day has two types at once. Clicking the tile shows the next two weeks in detail; a "Toon de rest van het jaar" ("Show the rest of the year") button then unfolds the remainder.

14

Indoor and outdoor climate: the measurement layer beneath the heat pump

Five Netatmo modules hang around the house: Woonkamer (living room), Slaapkamer M&M (the M&M bedroom), Bijkeuken (the utility room — that's the base station), Achtertuin (the back garden, outdoors) and a Regenmeter (rain gauge). Each module measures some subset of temperature, humidity, CO₂, air pressure, sound level, rain and battery level — which subset depends on the module: only the Bijkeuken measures air pressure and sound, the Achtertuin measures no CO₂, and the Regenmeter measures rain and nothing else.

Homey_Log doesn't collect those measurements through Homey, but straight from Netatmo. Not as isolated snapshots, but as a slice of the measurement series: every hour, each module's last four hours are fetched. Every measurement carries the timestamp Netatmo itself attached to it, not the moment Homey_Log picked it up. That has two pleasant consequences: fetching the same measurement twice never produces a duplicate row, and a module that has locked up or run flat stands out, because quite simply no new timestamps keep arriving.

Those four hours overlapping is exactly the point. Anyone who simply polls the current reading every ten minutes has a silent bug in the house: if a request fails, that measurement is gone for good — after all, the next request returns the new value, not the missed one. That is not theory; when Netatmo spent a while rejecting a quarter of all requests, 21 of the 96 quarter-hours went missing in a single day. Each round now repairs the previous three, and it costs not a single extra request: six per hour.

Beneath that lies a safety net for whatever slipped through anyway — an outage that lasted longer than four hours, or a server that was down for a night. Every night at half past four, Homey_Log walks through the day that is 24 to 48 hours old and fetches any missing pieces after the fact. If that succeeds, there's one line in the log; if it fails, that measurement is permanently lost and gets reported as an error instead of quietly vanishing.

The Netatmo card in Settings shows how long ago the last successful measurement came in, what the nightly check found, which modules Netatmo reports as unreachable, and the most recent outage if there is one. That's also where the four controls live: how often to fetch, how far to look back, from what interruption onward something counts as a gap, and what time the nightly check runs.

Each module can also be paused individually, just like every other measuring point in the house — a dead battery in the rain gauge shouldn't produce the same warning for days on end while nobody's home to replace it. Here too, pausing only silences the notifications: the collector keeps fetching and storing, so the module announces itself again on its own the moment it comes back to life.

When the manual history script (below) runs, it pauses the live collector for the duration of that run: both at once would fight over the same access token. That pause is an expiry date in the database, not an on/off switch — if the script crashes, the pause simply lapses instead of silently leaving the climate measurement parked forever. While the pause is active, the Netatmo card shows it as a separate, neutral notice — deliberately not as an outage, because it isn't one.

The link runs through a dedicated Netatmo app with a refresh token in the secret store; the collector maintains that link itself and recovers on its own if Netatmo rejects the access token once. If the refresh token turns out to be genuinely broken, that produces a single phone notification — and another one only after six hours, not on every poll. That notification distinguishes between two failures that look identical from the outside but have opposite remedies: a worn-out refresh token (a fresh token needs pasting in) and rejected client credentials (in which case a fresh token is precisely what won't help, and the client id or the secret needs checking). Netatmo itself says which of the two it is. Lumping both under a single "token refresh failed" heading costs half an hour of searching in the wrong direction — that has happened once. Following on from that, the input field now also validates the shape of the client id — 24 characters, digits and a–f — so a half-copied value doesn't get saved without a word. All of it viewable in a dedicated Climate tab in the House group, with a timeline per room. By default only temperature is shown; humidity, CO₂, air pressure, sound and rain can be toggled on. Alongside the five Netatmo rooms there is a sixth: the meter cupboard, measured by the weather station's gateway that hangs there anyway. That series comes from a completely different source, but the climate layer is a list of adapters — so a room keeps one continuous history even when the meter underneath it changes. Alongside the individual measurements, Homey_Log also keeps a per-quarter-hour summary — meant to be laid next to energy consumption — recording per quarter-hour both how many measurements sit underneath it and how many of those came from the historical fetch. That second number matters because the history sits at half-hour granularity while the modules themselves measure roughly every five minutes: from the measurement count alone, a quarter-hour from today is indistinguishable from a quarter-hour from 2015.

That difference in granularity is no footnote. Rain is counted as "how much fell since the previous measurement", so those numbers get summed. Fetch only every other measurement and a short shower disappears from view entirely — measured against a drizzle of 0.1 mm that was missing from the series while Netatmo still had it. That is why a slice of the series is fetched rather than a snapshot. Rain figures from before August 1, 2026 still sit at the half-hour granularity of the historical fetch — within that period they are internally consistent, but they can't be compared one-to-one with the fine-grained series that follows.

Why this exists

Without indoor and outdoor temperature, "the heat pump used 12 kWh" is a number without meaning. With those two, it becomes "12 kWh to keep the house at 20 °C when it's 2 °C outside" — and that is a number you can actually work with. This measurement layer is the foundation for the heat pump analysis in the next chapter.

The history has been backfilled to January 1, 2024: over 218,000 measurements across all five modules, contiguous from that date to today. Netatmo has even more waiting — Bijkeuken from July 7, 2015, Woonkamer from February 5, 2018, Achtertuin from December 11, 2019, the Regenmeter from June 2, 2022 and Slaapkamer M&M from November 3, 2023. That older part has been left where it is on purpose; the script to fetch it sits ready, and Marc decides if and when that happens.

15

The heat pump from the inside

The heat pump is by far the biggest consumer in the house, and in the energy books also the quietest: a kilowatt-hour figure comes in, and that is it. A NIBE SMO 40 knows a great deal more internally — flow and return temperature, how hard the compressor is working, whether the electric auxiliary heating is released, which price level it thinks it sees. All of that sits on the unit's own bus, and that bus is read along by a small box near the pump that forwards the messages to Homey_Log.

Every five minutes the collector walks through 34 registers — one question per register, neatly in turn, because the pump answers only one at a time. Such a round takes a good minute and yields one row: all the values together, plus how many registers did not answer and how long the round took. Those last two numbers aren't a by-product but the monitoring itself: a pump that stops answering looks different in the chart from a pump that is standing still, and that difference has to stay visible.

That register list is at the same time the only place recording, per register, what its number is, how it scales, which column it lands in and how it may be summarised over a period. Adding a register is therefore one line: the column, the storage and the series follow by themselves. Registers marked as a setting also produce a log line the moment somebody turns them.

On the Heat pump tab those series sit side by side, with a status card above them summarizing where things stand: is the compressor running, how many degrees between flow and return, is there an alarm. Alarms and fault buttons also announce themselves on the phone — when the alarm number changes, when an alarm has passed, and when the pump stays silent longer than agreed. The same pump appears on the phone as a tile: flow temperature, outdoor temperature, return, compressor frequency, and one word for what it is doing — at rest, heating, cooling, defrosting, hot water. That word is derived on the server from three registers, with defrosting beating everything else; the phone derives nothing itself. If the last successful reading is older than three measuring rounds, the tile goes grey and shows the age of that reading instead of a state — presenting an old value as fresh is precisely the mistake that went unnoticed here for two and a half days once before.

The pump draws its own heating curve

The heating curve — how hot the water has to get at which outdoor temperature — is a setting with two knobs: a curve number and an offset. The shape of that curve appears in no register at all; it lives in the pump's firmware and nowhere else. What the pump does give away is the point on the curve right now: which flow temperature it computes at the current outdoor temperature. Every outdoor temperature that passes therefore yields one point, and over a cold spell the pump draws its own line.

The chart groups those points per combination of curve number, offset and whole outdoor degree, and takes the median of them — not the mean, because a single outlier would otherwise lift the whole line. The current setting sits in front, earlier combinations stay visible as their own line, and an offset shift can be drawn ahead because one step is a fixed number of degrees. A different curve number is not: that changes the shape, and you only know the shape once the pump has walked it. The lower and upper limits the pump imposes on its own flow temperature are read as registers and bound the expected line.

Observe and advise, never control

The scope is fixed here, and it is narrower than what is technically possible: Homey_Log reads the heat pump and has an opinion about it, but turns no knob at all. In this house comfort outweighs the last few percent of efficiency, and a chilly living room is not an acceptable price for an experiment. That isn't temporary caution: the winter exists to measure, and the question of whether the house may adjust anything only comes up once there is a season's worth of figures to base that adjustment on.

Efficiency instead of consumption. With flow, return and compressor behavior next to the power draw, the question is no longer "how many kilowatt-hours" but "how much heat per kilowatt-hour" — the COP over the day and over the season. It is computed over whole quarter-hours, never over a half-filled bucket: a window that starts mid-quarter counts a sliver of heat whose electricity falls outside the frame, and that flatters the figure.

How it felt counts just as much. A winter in which only kilowatt-hours were written down yields an efficiency chart in spring that nobody can use: was the house pleasant back then? That is why a winter log sits next to the measurement layer. The phone app holds six buttons — too cold, fine, too warm, for the living room and for the bathroom — and one tap records how it felt at that moment, and who said so. You read them back on the Climate tab, below the chart, so a verdict always sits next to the temperature of that moment.

And because a button nobody thinks about produces no data, the app occasionally asks by itself — but only on an evening where the answer is worth something: when it was cold outside, when something was changed on the pump that day, or when the electricity price stood high for a long stretch. At most once a day, and not to anyone who already reported something. This too has an end date written into the code: it exists to calibrate, not to keep asking.

16

The garden: soil moisture instead of a clock

An irrigation system on a timer waters because it's Tuesday. In the ground there are now eight wireless moisture sensors, spread over the spots that matter independently of one another — borders, pots, lawn — measuring what actually happens at the roots. They report to their own gateway indoors, and Homey_Log picks up the readings every quarter of an hour.

On the Garden tab each spot has a tile with its current moisture level and a verdict in plain language: no need, maybe later, or yes now. Below that lies the day view, which draws not just the measured course but also what is still coming — expected rain as a band, evaporation as its own series, and the expected moisture course as a dotted projection. That projection is learned rather than calculated: the house's own series shows how many percent this soil drops per millimeter of evaporation and how much it rises per millimeter of rain, and with those two numbers the rest of the day can be estimated.

The rain and evaporation forecast comes from the same weather model that supplies the solar forecast. That is not coincidence but thrift: one source, one fetch path, and a forecast that can be checked here just as well as there — what the model said yesterday about today sits stored next to what the rain gauge actually counted.

Silence is the good news here

The garden has one watcher that speaks up on its own: a sensor that stays quiet too long, or a moisture level that sinks through the floor. It looks every quarter of an hour and only reports when something is wrong. All other garden information is there to be looked up, not to come by uninvited — a house that reports every morning that the border is still moist teaches you within a week to ignore its notifications.

What deliberately isn't there yet is the tap. The irrigation controller is ready, but the order that defines this whole project applies here too: measure for a season first, then hang something on a tap. There is also a known pitfall to solve up front — automatic irrigation looks, to the water meter's leak detection, like exactly the thing it is meant to warn about.

17

Where everyone — and every car — is

A house that wants to know whether the solar surplus can go to the car needs to know whether that car will still be there later. And an analyst explaining consumption gets more out of "nobody was home" than out of any chart. Loose indications are plentiful — Homey knows whether someone's phone is on the wifi, the car integration knows the cars' coordinates, and indoors a net of small Bluetooth receivers hears which phone is where — but an indication is not yet an answer. So there is exactly one answer per subject: four family members, two cars, and the house itself, each with the source that says so and how old that observation is.

That answer is deliberately layered rather than averaged. The best thing a system can say is a named place: home, work, the sports club. If it can't do that, then a coordinate with its accuracy attached. Failing that, an indication ("probably not home"). And otherwise, honestly: unknown. Each layer is one step less certainty, and that step is visible rather than averaged away. There is deliberately no 0-to-100 confidence score in which doubt can disappear: the outcome has to be explainable in a single sentence. With "home" a room can be added, but that is not a fifth layer: it is an attachment to that one named place, and only the Bluetooth net is allowed to fill it in.

A snapshot and a state are not the same thing

The distinction this system rests on: some sources measure where something was at that moment, others report a change and then go silent. A phone position is a snapshot and goes stale — a fix from hours ago says nothing about now. Homey's presence is a state: it reports once that you're home and then says nothing for days, and that silence means "nothing changed," not "nothing known." Treat both the same way and you get either a house that loses track of you at night, or a house that insists you're home while you're abroad.

Every source therefore has its own staleness window, configurable per source. The phone's is set to six hours rather than fifteen minutes, and that was measured: a moving phone reports roughly every minute, but a phone lying still can go quiet for hours. With fifteen minutes, someone on vacation would fall back to "probably not home" while a perfectly good position an hour old was sitting right there — a worse answer than the position with its age attached. This is safe because a named place always beats a coordinate: a stale position can never start claiming "home." A state source additionally gets a twelve-hour safety net, because otherwise a Homey that falls over at night would still be shouting "home" the next afternoon. For the room that safety net is shorter — twenty minutes — and to stop that very net from taking someone's room away while they sit still, the system writes a sign of life every ten minutes for as long as the device is simply being seen. And the house itself is not a measurement but a fact: it stands where it stands and never goes stale.

One layer deeper: which room

"Home" is too coarse for half the questions. So ten small Bluetooth boxes running open firmware hang around the house — six downstairs (two in the living room, kitchen, office, utility room, garage) and four above them (hall, a bedroom, bathroom, ironing room) — listening for the family's phones and watch. They talk to a message broker that runs inside the app itself, so there is no separate service and no Homey app in between. A box knows only its own name; which room that is lives in the mapping layer, and the list of rooms comes from the 3D model of the house.

Signal strength is not distance, and the boxes differ from one another by as much as 20 decibels in sensitivity. Each box is therefore calibrated: one minute with a device at one meter, and the median that comes out is that box's zero point from then on. Only after that is "the nearest box" a meaningful statement. Readings are folded into one median per minute, the nearest box within four meters wins, and a switch only counts after a minute of dwelling and a meter of difference — otherwise the room bounces back and forth between two equally strong neighbours. If readings do come in but none within range, the system lets the room go after three minutes: better "no room" than the room from half an hour ago. And between two devices belonging to the same person, the one that is moving wins — a phone on the charger says less than a watch walking along.

What is kept is a stay: per person a continuous period in one room, with which device tipped the balance and why. The raw readings are diagnostics and may eventually go; the stays are never cleaned up; and a device nobody enrolled is counted but not stored.

Taking part is your own choice, per person: under Mijn instellingen → Locatie delen ("My settings → Share location") you enrol your own phone, and who that is the app always reads from your sign-in, never from the request itself. Anyone enrolled gets the Waar ben ik ("Where am I") tile on the phone, where one tap lets you state which room you are really in. That is not a toy: it is the ground truth against which the system measures its own hit rate.

The sources are Homey's presence, the phones — via OwnTracks and via the app itself — the car telemetry, and the Bluetooth net indoors. Which source takes precedence for which kind of question is configuration, not code. Every transition from one prevailing place to the next is moreover recorded as an event: arriving and leaving, with the source and the layer attached, so that "nobody was home" is not a feeling but a statement with a timestamp. The Location page shows, per subject, the answer with its layer, its source, and its age, and whether the sources disagreed — a conflict is shown, not quietly smoothed over. Places you draw yourself on a map: a circle around the house, around work, around the sports club, each with a name. On a phone, each car gets a button labeled "navigeer hierheen" (navigate here) that opens the device's map app — and that button only exists on the phone, because on the wall tablet an external link would open a tab that never goes away.

Location data sits entirely behind the security layer: without a login or key, the app serves nothing here, not even to its own wall tablet. It's the only place in the system where the absence of credentials doesn't produce an error message but a block that simply isn't drawn at all — who is where, exactly, shouldn't be half visible.

18

Where this is headed

The biggest subject is measuring for a whole winter. The heat pump gives its figures, the house gives its verdicts on how it felt, and the weather stands next to both — and only once a full heating season of that exists is there something to build on. What comes after is the reversal this project has had in mind from the start: the house gets policy instead of loose rules. Not "set the charger to six amps", but "comfort comes before efficiency, and within that comfort make it as cheap as possible". A decision-maker tests what the house intends against that policy, first for months in the shadow — it writes down what it would have decided, and nobody carries it out — so that there is evidence before a single knob ever moves. That is work for the spring; this winter is for measuring.

Closer at hand lies the conductor's first switch. It already plans everything and already measures itself afterwards, and the question is no longer whether it can but whether it manages ten comparable days on which it is demonstrably no worse than what runs today. The first appliance to clear that yardstick gets genuine control — the water heater is the most likely first, because a tank of hot water makes the friendliest mistake there is. After that the home battery joins as a participant: that is today the biggest area where the conductor's plan and the existing quarter-hour plan still compute past each other.

Beyond that, the digital twin grows outdoors — garden and garage added, with the charger, the outdoor cameras and the moisture sensors in their real place in the model. And the garden's tap is still waiting for a season of figures, in the same order that marks this whole project: measure first, then hang something on a tap.

The overarching goal: full insight into what the house is doing, and a house that periodically audits itself, with proposals a human reviews — not a house turning the knobs unsupervised.