Electrolux-to-MQTT
I promised a proper write-up of Electrolux-to-MQTT (Message Queuing Telemetry Transport) back when this site launched. Here it is — less "what it does" (the project page covers that) and more of what it took.
The short version: a TypeScript service that polls Electrolux appliances through their official developer API (Application Programming Interface) and republishes everything to my own MQTT (Message Queuing Telemetry Transport) broker, with auto-discovery into Home Assistant. I built it because I kept running into issues with the existing integrations and got tired of waiting for fixes. It now runs as a Docker container for a small but real user base — the README's user badge is live telemetry, and watching it climb past ten has been one of the quiet joys of this project.
The "public" API (Application Programming Interface) that isn't quite
Electrolux has an official developer API (Application Programming Interface). What it doesn't have is an official way for a headless service to log in. So the bridge does what the web app does: fetch the login page for a CSRF (Cross-Site Request Forgery) token, POST the credentials, extract an authorization code, exchange it for tokens. The login endpoint accepts two different payload structures, and which one works appears to vary — so the client tries the nested shape and retries with the flattened one on invalid_request. Tokens live twelve hours and exist only in memory; I deliberately dropped the on-disk tokens.json early on, trading a re-auth on every restart for one less credential-shaped file on disk.
The API (Application Programming Interface) itself has personality:
- It caches state server-side. The first poll after sending a command returns the pre-command state, which would overwrite the correct value in Home Assistant with stale data. There's a config option —
commandStateDelaySeconds, default 30 — that exists purely to wait out their cache. - It fails slowly. The state endpoint can return HTTP (Hypertext Transfer Protocol) 500 after ~15.5 seconds. The client timeout is deliberately longer than that, so the real upstream error lands in the logs instead of a vague client-side timeout.
- Appliances flicker out of existence. A perfectly healthy discovery response occasionally just omits a device. Tearing down the HA (Home Assistant) entities on the spot would thrash them, so removal requires sustained absence — 30 minutes across successful responses, with API (Application Programming Interface) failures never starting the clock.
The bug that wouldn't self-heal
My favorite war story is the "Unavailable" climate entity. After a Home Assistant restart, entities showed Unavailable — expected, since state isn't retained and the bridge only publishes on change. The fix was to listen for HA (Home Assistant)'s birth message and republish. Except the internal cache holds a union of two shapes — the raw API (Application Programming Interface) response after a poll, and a normalized state after command feedback — and the republish sent the raw cached shape verbatim. HA (Home Assistant)'s templates expect the flat normalized fields, so the entity stayed Unavailable… and kept staying Unavailable, because the next poll diffed against the cache, saw no change, and published nothing. A wrong publish with no self-healing path. The fix came with an invariant that's now enforced in code: every cache-to-MQTT (Message Queuing Telemetry Transport) path goes through one normalization function, full stop.
Honorable mention: the analytics backend. Aptabase's ingest endpoint validates sessionId as a GUID (Globally Unique Identifier) and silently drops non-conforming events — returns 200 {}, writes nothing, errors nowhere. Every event from older bridge versions vanished into that behavior, and the diagnosis only became decisive by running SELECT * directly against ClickHouse and comparing landed rows.
Decisions I'd defend
No auto-update. Tempting feature, but an app that pulls and swaps its own Docker image needs the Docker socket mounted inside the container — host-root-equivalent access, which breaks the whole hardened posture. Updating is the orchestrator's job; the bridge instead tells you about updates: an MQTT (Message Queuing Telemetry Transport) info topic, a ready-made HA (Home Assistant) automation for phone notifications, optional ntfy.sh push with a one-hour soak delay so I can pull a broken release before anyone installs it.
CalVer (Calendar Versioning). Versions are YYYY.M.MICRO, Home Assistant style. The migration off SemVer (Semantic Versioning) meant replacing the release tooling, and the new changelog generator was validated by reproducing the old output byte-for-byte before switching.
Honest telemetry. The user badge counts unique installs over a rolling 26-hour window (24h max poll interval + 2h slack), with a stable install ID derived by one-way-hashing the account username — disclosed in the README, opt-out supported. Getting a steady count turned out to be a design problem of its own; daily-rotating IDs make every midnight look like an exodus.
The maintenance machinery
This repo has also become my proving ground for AI (Artificial Intelligence)-assisted maintenance. The .claude/ directory ships committed agent memory — 33 files of API (Application Programming Interface) quirks, audit heuristics and past decisions that every session inherits — plus hooks that do things like hard-block commits on a branch that's behind origin (a lesson learned by once duplicating a day of upstream fixes). There's a completed-but-shelved branch that replaces polling with Electrolux's server-sent events stream; it works, 840 tests green, and it stays unmerged until I trust it in soak. Knowing a finished thing can wait is also a maintenance skill.
Repository: gitlab.com/KirboDev/electrolux-to-mqtt · Docker Hub: kirbownz/electrolux-to-mqtt. MIT (Massachusetts Institute of Technology), contributions welcome — adding support for more appliance models is documented in the contributing guide.