Dobby·Guide github.com/mhyrr/dobby

The house, written down

Dobby's understanding of the house is not hidden in a model prompt. It has an explicit source of truth.

One house model, four ways to write it

The shared thread, the forms on /house and /admin, an outside agent over MCP, and a text editor can all change the same model. One writer validates and serializes every application change.

YAML is the current durable representation. It records which devices Dobby manages, what the household calls them, how they bind to Home Assistant, which settings narrow their capabilities, and how the Dobby server should run. The file never holds a secret, so you can share it.

In development, DOBBY_HOME_MANIFEST names the file. In an install it is /opt/dobby/config/home.yaml. Start from the example:

cp config/homes/example.yaml config/homes/my-house.yaml

Two sections. house: is your house. system: is the box Dobby runs on.

The house

house:
  id: my-house
  name: Our House
  timezone: America/New_York

  home_assistant:
    url: env:DOBBY_HA_URL
    token: env:DOBBY_HA_TOKEN

  devices:
    # …one entry per device, below…

Credentials are references. token: env:DOBBY_HA_TOKEN names an environment variable. Dobby reads it at boot. If the variable is missing, Dobby stops and names it instead of starting a house that cannot reach anything.

A device entry

Every device has the same parts. Only id, type, name, and bindings are required.

KeyWhat it is
idHow Dobby refers to it, forever. Convention: type:place, like thermostat:hall.
typeOne of the semantic types below. A type describes what the device means in the house, not who made it.
nameWhat the household calls it.
aliasesOther names people use for it: "the thermostat", "downstairs".
bindingsWhich Home Assistant entity is which part of the device. Copy entity IDs exactly.
hands_onlyOptional. Set true when Dobby and agents over MCP may read the device but may not command it. Card controls and schedules made in /admin still work.
settingsHouse rules. Optional. They can narrow what the hardware allows, never widen it.

Entity ids are in Home Assistant under Settings → Devices & services → Entities. A device bound to a wrong id shows NOT KNOWN and stays that way. Verify names it.

hands_only names the authority that remains. It does not mean the device is absent or globally read only. The language layer can still answer status questions. The command gate lives in the shared device protocol, so the same refusal applies to Dobby's model, MCP, and a delayed schedule they authored.

The device types

Home Assistant owns brands, accounts, radios, and protocols. Dobby owns the household contract. A Sonos room and a Cast speaker are both speaker. A Ring and a Reolink doorbell are both doorbell. Add one device entry for each household object you want Dobby to manage.

Some Home Assistant devices expose several entities. Keep them in one Dobby entry. A doorbell can bind its ring event, camera, and motion sensor. An environment monitor can bind temperature and humidity. Discovery proposes the complete binding map when Home Assistant says those entities share one device.

thermostat

Dobby reads it and sets it. The hardware's range comes from Home Assistant. settings narrows that range to what your house allows anyone to ask for.

- id: thermostat:hall
  type: thermostat
  name: hall thermostat
  aliases: [the thermostat, downstairs thermostat]
  bindings:
    climate: climate.hall
  settings:
    min_temperature_f: 60
    max_temperature_f: 78

With this range, nobody can set the hall to 90. Not a person at a card, not the model, not an agent over MCP. The thermostat shows HELD with the reason beside it.

light

Dobby switches it, and dims it if the bulb can dim. The bulb reports that itself. Nothing to set.

- id: light:living_room
  type: light
  name: living room light
  aliases: [living room lamp]
  bindings:
    light: light.living_room

The common library

TypeBindingsWhat Dobby offers
speakermedia_playerStatus, play, pause, and volume. Sonos is an example integration, not a separate type.
cameracamera; optional motionStatus and motion. Streams and snapshots stay on Home Assistant's authenticated media path.
doorbellevent; optional camera and motionThe last ring event, camera availability, and motion.
locklockStatus and secure. Dobby does not offer unlock.
access_covercoverStatus and close for a garage door, gate, powered door, or window. Dobby does not offer open.
power_switchswitchStatus, on, and off for a plug, outlet, or relay.
shadecoverStatus, open, close, and position for a blind, curtain, shutter, shade, or awning.
fanfanStatus, on, off, and speed when the device reports speed support.
environment_monitorOne or more of temperature, humidity, carbon_dioxide, air_quality, pm25, volatile_organic_compounds, and illuminanceReadings and units. Read only.
contact_sensorcontactOpen or closed for a door, window, or garage contact. Read only.
occupancy_sensoroccupancyMotion, occupancy, or presence. Read only.
safety_sensoralarmSmoke, carbon monoxide, gas, water, heat, or cold alarm state. Read only.

The action limit is part of each type. It does not depend on the model behaving well. Locks cannot be unlocked, access covers cannot be opened, and safety sensors cannot be actuated through the thread, a schedule, or MCP.

Household appliances

Home Assistant handles manufacturers, models, accounts, and connections. Dobby binds the resulting entities to household device types. Floor heating uses thermostat; a standalone freezer uses refrigerator.

TypeBindingsWhat Dobby offers
water_heaterwater_heaterTemperature, mode, away mode, and power where HA reports the capability. Configure settings.temperature_unit to match HA's system unit when the entity omits it.
humidifier, dehumidifierhumidifier, with the matching HA device classPower, humidity target within the reported range, and advertised modes. Household bounds can narrow the range, and the fader on the card narrows too.
air_purifier, range_hoodfan; optional filter_remaining and filter_duePower, supported speed, and filter readings. Bind a hood light as a separate light.
dishwasher, washer, dryerExplicit cycle, door, progress, and finish-time sensors; remaining time on the washer and dryerStatus only. A remote-readiness flag does not start a cycle.
oven, refrigerator, wine_coolerExplicit measured-temperature, setpoint, door, or activity sensorsStatus only. A setpoint does not establish a measured temperature.
coffee_maker, ice_makerExplicit activity, supply, or maintenance sensorsStatus only. Missing readings remain unknown.
cooktop, microwaveExplicit activity, residual-heat, door, or cycle sensors as applicableStatus only. Use one cooktop entry per heating zone when HA reports separate zones.

Use the bindings shown in the example house. Purifiers, hoods, and sensor-based appliances need explicit entries because HA entity names do not establish their household role. Every sensor-based appliance needs at least one reading binding.

vacuum

Dobby starts it and sends it home.

- id: vacuum:downstairs
  type: vacuum
  name: robot vacuum
  aliases: [the vacuum, the roomba]
  bindings:
    vacuum: vacuum.downstairs

wifi_endpoint

Something on the network Dobby can report as reachable or not: a printer, a TV, a speaker. Read only. The usual source is a binary_sensor from Home Assistant's ping integration.

- id: wifi:office_printer
  type: wifi_endpoint
  name: office printer
  aliases: [the printer]
  ha_integration: ping
  bindings:
    connectivity: binary_sensor.office_printer

A printer that drops off at three in the morning shows on its card and in the log. It does not post into the thread.

Standing rules

Definitions live under house.rules. The House page offers a typed form using each device's available readings. Changes made there apply immediately without restarting Dobby's conversation. Hand edits apply on restart. Proposals and notices are stored in the database.

rules:
  - id: main-room-cold
    name: Main room cold
    source: Tell me if the main room stays below 60°F for twenty minutes
    enabled: true
    device: thermostat:main
    kind: state
    attribute: current_temperature_f
    operator: lt
    value: 60
    duration_seconds: 1200
    window:
      start: "20:00"
      end: "07:00"
      days: [1, 2, 3, 4, 5]

The example belongs inside house. Device ids must match your house. Operators are eq and ne, plus gt, gte, lt, and lte for numbers. Environmental readings also require the exact reported unit. Unsupported readings, values, and fields are refused.

An absence rule uses kind: absence, event_kind: device_changed, and action: state_changed, alongside the desired attribute, operator, and value. Only a recorded change to that attribute counts; a battery update while a vacuum is cleaning does not count as another cleaning start. Absence rules for grouped environmental readings are not supported.

Rules only report. A hands-only device can still be watched. The limit is 100 rules per house and one year per duration. A state duration may be zero; an absence duration must be positive. A rule with a window must have a duration shorter than the window, because the watch starts over at every window edge. source is the household's own sentence when Dobby wrote the rule from the thread; a rule written by hand or from the form has none. For what notices and acknowledgments mean, see Living with Dobby.

The system section

All seven settings are optional. Three have an environment variable that wins while it is exported, so you can try something without editing the file.

system:
  model: openrouter:openai/gpt-5.6-luna
  reasoning: low
  routing: latency
  provider: together
  port: 4000
  lan: true
  hostname: dobby.local
KeyOverrideTakes effect
modelDOBBY_MODELAt the next reply. OpenRouter names use openrouter:author/model. Three have been walked: openai/gpt-5.6-luna, the default, z-ai/glm-5.3-flash, and z-ai/glm-5.2.
reasoning—At the next reply. How hard a reasoning model thinks before it answers: low, medium or high. Unset leaves the provider's default.
routing—At the next reply. What OpenRouter optimizes for when it picks the endpoint that serves the model: latency, throughput or price. It is an OpenRouter setting, so it needs a model reached through OpenRouter: set it beside a model that is not an openrouter: name and the house refuses to start, saying so. That is deliberate — the alternative is a house that starts and then cannot answer anything.
provider—At the next reply. The one OpenRouter endpoint that serves the model, with no fallback to another, named by the slug OpenRouter's endpoint listing uses: together, io-net/fp8, amazon-bedrock/us-east-1. Set it when a measurement has chosen an endpoint and you want the same one every time; routing lets OpenRouter choose per request, and the same model's first word moved from half a second to three within one hour on that setting. With a pin in force the routing has nothing left to choose. An endpoint that stops serving the model refuses the reply rather than falling back, which is the point of a pin. Like routing, it needs a model reached through OpenRouter.
portPORTOn restart.
lanDOBBY_LANOn restart. Binds every interface and advertises the hostname.
hostname—On restart. One label and .local. Dobby answers only to this name, localhost, PHX_HOST and LAN addresses; any other name goes in DOBBY_ALLOWED_HOSTS. See running Dobby.

routing: latency is the default: OpenRouter picks whichever endpoint looks fastest for each reply, and that is how the local house runs. provider is for a household that wants the same endpoint every time. Which endpoint serves a model decides how long the first word takes, more than the model does. On 2026-09-07 every endpoint under each of the three models Dobby is run with was timed on the same requests, pinned one at a time with reasoning: low. The fastest to its first token, measured from the model call, and in the last column the slug worth writing if you pin, which is not always the first to speak:

ModelFastest first tokenFirst token, medianWorst turn"Set the thermostat to 70", end to endprovider worth writing
z-ai/glm-5.3-flashwafer0.5 s1.0 s2.2 sio-net/fp8
z-ai/glm-5.2together0.3 s0.5 s1.1 stogether
openai/gpt-5.6-lunaamazon-bedrock/us-east-10.7 s1.9 s1.3 samazon-bedrock/us-east-1

The same model on its slowest endpoint took four to twelve seconds for the same request, which is why the pin exists. Wafer got its first word out soonest under Flash and finished both requests slower than io.net, with the lowest listed uptime of the twenty-four, so a household pinning Flash would do better with io-net/fp8. Two things to know before writing a pin. A pin has no fallback: an endpoint that is rate-limited or down refuses the reply instead of handing it to another, and on the day of the measurement nine of Flash's twenty-four endpoints were refusing for a minute at a time. And the numbers are one afternoon's; the eval that made them (test/dobby/eval/provider_eval_test.exs) reruns in a few minutes per model when they look stale. GLM 5.2 costs about twenty times Flash per token and answers in half the time; Luna sits between them on both. Luna stays the default because Dobby is built and tested against it first.

The same seven settings are a form on /admin. After a save, each field says whether it took effect now or waits for a restart.

Dobby writes this file too

The forms on /house and /admin, a device Dobby proposes in the thread, and an agent over MCP all write to this file. One writer handles every change, so two saves at once queue instead of colliding. Two things to know before you edit by hand:

What you should see

# This file is written by Dobby, and rewritten in full every time the # house or the system panel is changed. Comments and key order do not # survive that — the file is the record, not the document.

The header Dobby puts on a file it has written. If your file starts with it, something other than your editor has saved it.