A rain-soaked robot vacuum leans out of dark swamp water through reeds and duckweed, its cyan status lights glowing beneath an amber work lamp on a weathered timber post.

My robot vacuum had 7,625 MQTT connection errors before I noticed it was faithfully trying to reach the wrong address.

That was the easy part. I corrected the broker address, watched Valetudo report ready, and suddenly my Dreame L10s Ultra was publishing its whole little world again: battery, dock state, attachments, presets, rooms, errors, maintenance events, and commands.

The harder part was realizing that “available over MQTT” is not the same thing as “safe for an agent to use.”

Home Assistant already knew about the vacuum. Node-RED already watched parts of it. I could SSH into the robot if I really needed to. But those were three different routes into one appliance, and none of them gave an agent a clear answer to a simple question: what can this particular robot do, right now, and what is the safe way to ask it?

So I made @mgreten/valetudo.

Discovery before control

Valetudo publishes a Homie topic tree. The useful thing about Homie is that the robot does not only publish values. It publishes metadata describing the capabilities and properties it actually has: names, data types, units, enum values, numeric ranges, whether a value is retained, and whether it can be changed.

The extension starts there. Its discover method reads that advertised contract and writes it as Swamp data.

swamp extension pull @mgreten/valetudo

swamp model create @mgreten/valetudo robot-vacuum \
  --global-arg brokerUrl=mqtt://mqtt.example.net:1883 \
  --global-arg topicPrefix=valetudo \
  --global-arg identifier=my-robot

swamp model method run robot-vacuum discover

That matters because Valetudo runs on robots from several manufacturers, and their capability surfaces are not identical. A model that assumes every vacuum has the same dock, mop controls, fan presets, or map behavior would look convenient right up until it met a different robot.

discover makes the robot describe itself first. The agent can inspect the resulting capabilities instead of guessing from documentation, copying topics from my machine, or trying a command and hoping the payload happens to fit.

Status becomes data

The second method is getStatus. It reads a deliberately bounded set of retained topics and writes one normalized snapshot:

  • MQTT and robot status
  • current error description
  • battery level and charging state
  • dock state
  • dustbin, mop, and water-tank attachment state
  • fan, water, and operation presets
  • named map segments
  • active Valetudo events
swamp model method run robot-vacuum getStatus

The extension intentionally does not subscribe to the whole Valetudo tree. Maps can be large, and “just subscribe to #” is an easy way to pull far more data than a status check needs. It asks for the small retained properties it understands, tolerates capabilities that are absent, and fails clearly if there is no retained status under the configured robot topic.

The result is not only terminal output. It is versioned Swamp data, which means a workflow can inspect the latest observed state, compare runs, or assert a postcondition without parsing logs.

That distinction became real almost immediately. Valetudo publishes its active maintenance events as a complete snapshot every thirty seconds. My existing Node-RED flow treated each snapshot like a new event and sent the same depleted-consumable notification over and over. The fix was to remember the stable event IDs and only forward newly appearing ones.

The robot was not spamming events. My automation had misunderstood the shape of the data.

A typed status resource does not magically prevent that kind of mistake, but it gives the mistake a name. A snapshot is a snapshot. A state transition is something you derive from two snapshots. That is much easier to reason about than a stream of anonymous MQTT messages.

Control, with a boundary

The action method supports the ordinary controls I care about:

  • start, stop, and pause
  • home and locate
  • auto-empty for a compatible dock
  • clean-segments with one to three iterations and optional custom order
  • set-property for robot-specific controls discovered through Homie
swamp model method run robot-vacuum action \
  --input action=clean-segments \
  --input 'segmentIds=["20","18","16"]' \
  --input iterations=2 \
  --input customOrder=true

The last action is the escape hatch that keeps the model general. If a robot advertises a settable property the extension does not have a friendly action for, set-property can address it directly. But it still checks the discovery metadata first. The capability and property must exist, the property must be advertised as settable, and enum or integer-range values must fit the robot’s declared format. MQTT wildcards are rejected.

Commands publish with QoS 1 and are never retained. After the broker confirms publication, the model records the validated topic and payload as an action result.

That receipt proves the command was accepted by MQTT. It does not prove a physical robot completed the action.

I wanted that limitation to be explicit. The correct follow-up for a workflow that needs proof is another getStatus and an assertion against observed state. “The broker acknowledged my message” and “the vacuum returned to the dock” are different claims.

The reusable part stayed small

The extension does not depend on Home Assistant, Node-RED, my Mosquitto deployment, my network layout, or my robot’s name. It needs a reachable MQTT broker, a topic prefix, and a Valetudo identifier. Credentials are optional and can stay in a Swamp vault.

My private setup still contains all the messy context: where the broker runs, which rooms have which segment IDs, which notifications I want, and what I call the robot. The public extension contains the reusable boundary: discover what exists, read what is retained, validate what can change, publish narrowly, and leave a receipt.

That is the part I keep wanting from personal automation. Not a universal robot-vacuum brain. Not an agent with SSH access improvising against a household appliance. A small contract that makes the useful operations legible and makes unsupported operations fail before they leave the machine.

The vacuum did not get smarter. The path between intention and MQTT got less vague.