A Smart Home That Feels Like Home
Having to get up again after a shower because the fan is still humming: no disaster, but one of those small annoyances that become noticeable over time.
We had a second one as well: when there was smoke in the outside air, the quickest way to switch off the ventilation was to go to the fuse box.
So the reason for this smart home was decidedly unspectacular. Our mechanical ventilation system has three separate exhaust units - one in each of the two bathrooms and one in the kitchen. Each runs in its normal mode and can be switched to boost mode for showering or cooking. The house was wired conventionally in 2014: three independent devices, no shared control line, no bus system. Until then, each unit had only its local rocker switch for its boost mode.
That made the goal clear: the ventilation should detect humidity on its own, remain operable locally and be quick to switch off in an exceptional case. No technology for technology’s sake, just a little less friction in everyday life.
The Most Important Principle: The House Remains Capable of Acting
The architecture therefore follows a simple idea: whatever needs to take effect immediately in the house should make its decision as close to the hardware as possible. An actuator that switches ventilation should also handle its basic function when the dashboard, broker or Wi-Fi is temporarily unreachable. The home controller makes states visible and relays requests; it is deliberately not an irreplaceable control center.
That is Local First not as a label, but as a failure strategy. If the broker goes down, buttons and humidity automation continue to work; if the dashboard restarts, retained MQTT messages refill its working memory. If a smart meter port briefly fails, the gateway reconnects without affecting other functions. Each failure is therefore confined to the comfort layer it actually affects.
Shelly was a good fit for this first step because the devices combine Wi-Fi, MQTT, local JavaScript automation and BLE sensors. The brand was not what mattered, but the capability: the solution needs neither a manufacturer account nor a proprietary hub to perform its core task. The documented device interfaces, MQTT settings and script features are what make this local integration robust in the first place.
Three Fans, Three Small Controllers
Behind each familiar button, a Shelly 2PM Gen4 with two outputs now does its work: one switches normal operation, the other boost mode. From the outside, the gesture remains familiar. Internally, however, a simple switch has become an interface that understands context.
A short button press starts boost mode for ten minutes. Another press extends it in increments, but never without limit: after one hour at the latest, the unit returns to its defined state. A long press ends boost mode. Holding the button for four seconds changes the normal ventilation request for that unit and, when the broker is reachable, passes it on to the other two. The former trip to the fuse box thus becomes a deliberately chosen home function - without reducing the local switch to a remote control for a server.
In the bathroom, a Shelly BLU H&T is added as a BTHome sensor. It measures temperature and humidity; if humidity rises significantly or quickly, the script requests boost mode. Hysteresis ensures that small fluctuations do not cause nervous switching. The manual timer and humidity automation are two independent reasons for the same output: if the timer ends while the bathroom is still too humid, boost mode continues. It ends only when there is no longer any reason for it.
This is precisely where the real strength of distribution becomes apparent. A decision can conveniently be shared across the house via MQTT, for example to switch off ventilation when there is smoke in the outside air. If the broker goes down, these house-wide shortcuts are unavailable. At each individual unit, buttons, timers and humidity control continue to work nonetheless.
Five Small Building Blocks Instead of One Big Black Box
The architecture consists of four deliberately lean components. Each has a responsibility that can be explained and specifically checked when something goes wrong.
| Building block | Responsibility | Why this boundary matters |
|---|---|---|
| Ansible | Binds IP addresses, devices, channels, sensors and domain names; renders and installs Shelly scripts. | Hardware knowledge lives in one place instead of being hidden in topics, UI code or naming conventions. |
| Shelly scripts | Switch ventilation and heating, process buttons and BLE sensors, and provide telemetry. | Time-critical processes work even without central control. |
| Mosquitto | Distributes MQTT messages and deliberately keeps retained states. | The broker connects components without becoming a hidden application server. |
| Smart meter gateway | Reads SML telegrams from serial ports, decodes OBIS values and publishes energy and power. | The meter head’s special protocol remains at a small, replaceable boundary. |
| Home controller | Provides a local web interface, permitted controls, MQTT monitoring and Prometheus metrics. | Operation, observation and the domain view remain separate from actuator logic. |
An Inventory That Describes Reality
One of the most important decisions is contained in Ansible. The inventory is not just an address book; it explicitly models the connection between the physical and domain worlds:
This prevents a typical smart-home mistake: guessing meanings from hostnames, IP addresses or channels. A Shelly can have several outputs, a heating circuit can supply several zones and a sensor can measure somewhere other than where its gateway is located. In the inventory, these relationships are explicit, checkable and versioned.
From this source, Ansible renders a suitable, compact JavaScript script for each device. Outputs, sensor IDs, thresholds, timers and domain MQTT topics come from the inventory; Ansible then transfers the result through the device API and restarts it in a defined manner. Changes therefore remain traceable, reviewable and deployable in a targeted way - and months later it is still clear why something is connected where it is.
The default configuration for each actuator is embedded directly in the generated script as a constant. After deployment, the Shelly has everything it needs for its basic function: no request to the home controller, no waiting on the MQTT bus, no configuration that exists only in a browser tab. This makes the local logic both self-contained and deterministic.
A key part of the concept is to describe as much as possible in versionable text files: the Ansible repository with its inventory and templates, JSON for the controller configuration and JavaScript for the Shelly scripts. A Git diff therefore shows not only changed code, but the physical function affected - from distribution through the firmware script and network address to the user interface.
This is also the best working foundation for AI. Apart from minor adjustments, the Shelly scripts emerged from domain-specific prompts and the target architecture described here; the smart meter gateway and home controller were developed in the same way. This works because requirements, contracts and existing logic are accessible as files. Behind a colorful web interface, these relationships would remain hidden from people, Git and AI alike.
MQTT as a Small, Clear Language
MQTT is not just a transport here, but a clearly defined language. The three namespaces answer three different questions:
| Prefix | Meaning | Write permission | Persistence |
|---|---|---|---|
state/ |
What is actually happening? | The respective producer only | normally retained |
config/ |
What should apply persistently? | The interface or local actuator | always retained |
cmd/ |
What should happen exactly once? | The interface | never retained |
This separation is small but effective. A dashboard cannot falsify an observed
state because it never writes to state/. A ventilation boost is not
accidentally repeated after a reconnect because it is not retained under
cmd/. A seasonal heating request, on the other hand, survives restarts
because it remains retained under config/.
Topic names also remain domain-specific. state/home/heating-circuit/<circuit>
talks about a heating circuit rather than switch:0 on a particular device.
Technical diagnostics such as RSSI or availability are kept separately under
state/device/. Hardware can therefore change without breaking the domain API.
A Broker That Stays Out of the Way
On the home server, Eclipse Mosquitto distributes the MQTT messages. The broker is exactly right for this job: small, economical in its resource usage, with a manageable text-based configuration and pleasantly unspectacular in operation. It accepts messages, delivers them to interested components and keeps states explicitly marked as retained ready for newly connected participants. It should carry no more application logic than that.
Those familiar with me and my other articles may be wondering: “Matthias, you are not using Pulsar for this?” No - at least not between three fans, a few sensors and the home controller. Apache Pulsar is an excellent event backbone when retention, replays, multi-tenancy or scalable backend processing are needed. I describe why MQTT and Pulsar usefully complement each other in MQTT Meets Pulsar: A Pragmatic Bridge for Modern IoT Architectures. For the current purpose in this house, however, Pulsar would add operational footprint without adding everyday value. Choosing a suitable building block is often a more demanding decision than choosing the most powerful one.
What the House Handles Itself Today
Ventilation was the beginning. Heating came next: underfloor heating, sluggish actuators, an even more sluggish heat pump and, after the summer, an occasional valve that preferred to seize up rather than start moving again. Automation cannot make this physics disappear. But it can show what is happening and move the actuators reliably.
- Ventilation actuators distinguish normal ventilation from boost mode, combine manual operation with humidity control and remain functional per unit.
- Bluetooth LE sensors measure room temperatures and provide them as MQTT states for display, automation and monitoring.
- Heating actuators know the target state, actual state and local override. In summer, a time-staggered valve protection routine moves the valves regularly.
- Shelly EMs and a small Go-based SML gateway publish power and meter readings. The gateway processes valid OBIS values and reconnects automatically after port errors.
The smart meter gateway runs on a Raspberry Pi in the distribution cabinet. Two USB IEC infrared read heads read the meters there over USB. A lean Buildroot system runs on the Pi alongside the Go binary: it reads SML from the serial interfaces, validates and decodes the OBIS values, and bridges them into the home network as MQTT states. Protocol translation thus remains right at the physical interface, while the rest of the system receives ordinary MQTT and Prometheus data.
The home controller sees all of this, but does not become a second source of truth. It keeps only timestamped values in memory. After a reconnect, retained messages provide an up-to-date view again. The interface uses embedded HTML, CSS and HTMX; the MQTT monitor also updates locally through small HTTP fragments. The resulting Go binary needs no internet access to operate.
Observability That Deserves the Name
Prometheus is not an afterthought, but part of the architecture. The home
controller exports only explicitly configured state/ values. This prevents
random topics or technical internals from becoming an uncontrolled flood of
metrics. Name, unit, type, labels and scaling are defined declaratively.
This produces data for useful questions rather than data collection for its own sake: How is energy demand changing? When does ventilation run, and for how long? Which change actually helped? The smart meter gateway provides power as a gauge and total energy as a counter. Both Go services deliberately remain small and can be shipped on x86_64 as well as ARMv7.
Not Against Platforms, but for the Right Boundaries
Cloud platforms are convenient when getting started. They often bring account lock-in, dependence on the internet, the risk of being tied to a manufacturer and a chain of integrations that is difficult to understand. A completely local, centralized instance also moves every rule decision into a single service. This project deliberately chooses a different compromise:
| Centralized or cloud-oriented solution | This architecture |
|---|---|
| A central process must be available for every automation. | Local actuators retain their core logic and last valid target state. |
| Integrations often mix device protocol, domain meaning and UI. | Inventory, MQTT contract, actuator logic and presentation have separate boundaries. |
| The data model and troubleshooting often emerge only over time. | state/, config/ and cmd/ make the owner, lifetime and intent of every message visible. |
| Convenience can mean an external dependency. | Operation, control and metrics work within the local network. |
| Large platforms solve many tasks at once. | Small, replaceable services each solve one task well. |
This is not a rejection of Home Assistant, OpenHAB or cloud products. They are powerful tools, especially when many heterogeneous integrations, voice assistants or ready-made dashboards are the priority. For this house, the focus is different: basic functions should remain independent of a comfort layer. Showers, smoke, stuck valves and a network outage do not wait for a central instance to become available.
The Real Strength: Changes Remain Manageable
An architecture proves itself not on day one, but with the next change. In this project, every typical change has a clearly defined place:
- A new sensor, room or output is bound in the inventory and deployed only to the affected Shellys.
- A new dashboard tile or control is described in the controller configuration, not built into the actuator logic.
- A new meter receives a domain identity and a serial port in the gateway.
- A persistent request is expressed as retained
config/; an action as non-retainedcmd/.
This makes operations and troubleshooting pleasantly concrete. If a value is missing from the UI, the question is not “What is the platform doing?” but: Is the producer publishing a retained state? Is the broker reachable? Is the value stale? Has the controller explicitly configured the measurement? Every layer provides a clear answer.
What May Come Next - and What May Not
The architecture is deliberately open enough for additional functions: lighting or individual scenes can be added if they noticeably simplify a recurring action. A scene is then not a colorful button, but a clearly named, traceable intention - setting several states for a specific situation without having to operate every device individually.
The standard remains the same as for the ventilation: What problem disappears as a result, and does the solution work better in everyday life than the old switch? An additional actuator consumes power, increases the number of components and needs to be operated. Remotely switching a ceiling light in a storage room would usually be a poor trade-off. Where operation, safety, energy awareness or accessibility genuinely improve, expanding the system is worthwhile. The house should become more useful, not merely contain more technology.
A House That Does Not Feel Alien
Technology in the home should strengthen self-determination, not create new dependencies. This project did not begin with a desire for an app, but with a fan that kept running, smoke in the outside air and a heating system whose state no one really knew.
What emerged was not an off-the-shelf product, but an understandable foundation that belongs to the house and can grow with it. It responds locally, remains clear and keeps working even when nothing is available outside its own four walls. That is exactly how a smart home should feel: helpful, but never alien.