Skip to content
ponderance / anmol
← Marginalia

homelab · engineering

Teaching Home Assistant to watch my 3D printer

I wanted the house to tell me when a print finished. One line of automation. Getting to that one line was a tour of every place a 'simple' integration hides a sharp edge, starting with the discovery that 'printing' is spelled running.

2026-06-22

I wanted one thing. When a print finishes, the house should tell me. That’s the entire feature. It turned into a guided tour of every place a “simple” integration keeps a sharp edge, and I have the small scars to show for it, mostly emotional.

The base is genuinely easy. Home Assistant talks to the printer in LAN mode, local only, no round-trip to anyone’s cloud, using its local address and an access code. The entities show up prefixed with the printer’s serial, like sensor.x1c_<serial>_print_status. So far, so smooth.

The state isn’t called what you’d bet money it’s called. My first automation triggered on the status being "printing". It never fired. Nothing was broken. The sensor just doesn’t use that word. The “currently printing” value comes through as running:

# fires on completion, once you trigger off the REAL state words
trigger:
  - platform: state
    entity_id: sensor.x1c_<serial>_print_status
    from: "running"     # not "printing", the bit that ate an evening
    to: "finished"

The states come straight out of the firmware’s gcode-state vocabulary, and the lesson generalized immediately: read the actual enum a device emits, and never trust the friendly English word you assume it emits. The gap between “printing” and running was the whole bug, and it fails in the worst way, silently. A trigger that never matches looks exactly like a trigger patiently waiting for its moment. There’s no error. There’s just a notification that never comes and a you that slowly realizes it’s been an hour.

Native data beats scraping, every time. I first tracked firmware updates by scraping a status page on a timer, because that’s what I reached for first. Then I found the integration already exposes a native firmware entity, in LAN mode, that reports installed and latest versions directly. I deleted the scraper without ceremony. Structured data from the device beats parsing a page that can rearrange itself on any update and take your regex down with it, and it needs no extra credentials or polling target to babysit.

The camera needs a translator. The printer’s camera is an encrypted RTSP stream the browser won’t play on its own. A small restreamer re-publishes it as something a browser can play, with the printer’s credentials passed in from the environment instead of baked into a config file sitting on disk.

Bridge mode to dodge a collision. The print-archive dashboard’s default deployment wants a host port another service already owns. Its default mode’s main draw is auto-discovery on the network, but I add the printer by a fixed address, so I don’t need discovery at all. So it runs in bridge mode, publishing just its web UI. I gave up the one feature I wasn’t using and got the port back. Fair trade.

None of these were hard, and that’s the actual point I want to make. A “simple” integration is a little stack of very specific facts: the real state enum, the native entity that already exists, the stream format the browser will accept, the port map. The work is reading each one instead of assuming it. The automation that finally tells me a print is done is a single line. Learning that “printing” is spelled running was the job. One word. One stupid, load-bearing word. And honestly it’s the kind of job I enjoy more than I’ll admit.

← back to the index