Dobby·Guide github.com/mhyrr/dobby

Bring Dobby home

First make Dobby work on one computer. Then give everyone in the house the same one.

You are done with this chapter when:

Prepare one computer

You need:

git clone https://github.com/mhyrr/dobby.git && cd dobby
mix setup
cp config/homes/example.yaml config/homes/my-house.yaml
# edit my-house.yaml: your house name, timezone, and managed devices

The example explains each field. The house, written down is the complete reference.

Give Dobby its credentials

The house file refers to its credentials by name. Three variables cover a house:

export DOBBY_HA_URL=http://homeassistant.local:8123
export DOBBY_HA_TOKEN=...
export OPENROUTER_API_KEY=...

In development, copy .env.example to .env and fill it in. Variables exported in the shell still win. mix test ignores all of them: the tests run against a fake Home Assistant, always.

Verify, then run

mix dobby.ha.verify connects to your Home Assistant and prints every managed device once the first state sync lands. One command checks the connection, the token, the subscription, and the aggregate state path into each managed device.

DOBBY_HOME_MANIFEST=config/homes/my-house.yaml mix dobby.ha.verify

What you should see

[info] connected to Home Assistant 2026.8.2 at http://homeassistant.local:8123 home "my-house" with 2 device(s) %{ id: "light:living_room", name: "living room light", type: :light, available: true, power: :on, brightness_percent: 71 } %{ id: "thermostat:hall", name: "hall thermostat", type: :thermostat, available: true, target_temperature_f: 70, min_temperature_f: 60, max_temperature_f: 78, current_temperature_f: 72, hvac_mode: :cool } initial state sync: ok

Every managed device available: true. If one stays unavailable, check its required binding IDs against Home Assistant. Compound devices can have optional camera, motion, or sensor bindings; inspect those readings separately.

--round-trip also moves a thermostat one degree and back, and waits for Home Assistant to confirm each step. It picks the first thermostat in the file unless you name one. It is a real change, so name a virtual thermostat if you have any doubt.

DOBBY_HOME_MANIFEST=config/homes/my-house.yaml \
  mix dobby.ha.verify --round-trip thermostat:demo

What you should see

discovered capabilities: %{min_temperature_f: 45, max_temperature_f: 95, hvac_modes: ["off", "heat", "cool", "auto", "dry", "fan_only"]} household policy: %{min_temperature_f: 60, max_temperature_f: 76} accepted range: 60–76 round trip: thermostat:demo setpoint 70 → 69.0 → back accepted 69.0; waiting for the house to confirm ... confirmed at 69.0 after 105ms accepted 70.0; waiting for the house to confirm ... confirmed at 70.0 after 104ms round trip: ok — setpoint restored to 70

What the hardware allows, what your house allows, and the range that results. Then two confirmations from Home Assistant. A virtual device answers in about a hundred milliseconds. A cloud thermostat can take as long as its polling interval.

Now start it.

DOBBY_HOME_MANIFEST=config/homes/my-house.yaml mix phx.server

What you should see

[info] connected to Home Assistant 2026.8.2 at http://homeassistant.local:8123 [info] Running DobbyWeb.Endpoint with Bandit 1.12.4 at 127.0.0.1:4000 (http) [info] Access DobbyWeb.Endpoint at http://localhost:4000

Open http://localhost:4000/. This machine only, until the next section.

The soul

config/soul.md is Dobby's voice: how it talks and what it cares about. Edit it like a letter. In an install it is /opt/dobby/config/soul.md; DOBBY_SOUL points anywhere else.

The soul is personality only. The rules that keep Dobby honest live in code, so no rewrite of the voice can remove them. Make it a grump or a saint. It still cannot tell you the room is warm.

The whole household

Set lan: true in the house file, or export DOBBY_LAN=1. Dobby then listens on every interface and announces the machine as dobby.local for as long as it runs.

What you should see

[info] Running DobbyWeb.Endpoint with Bandit 1.12.4 at 0.0.0.0:4000 (http) [info] the house answers at http://dobby.local:4000/ (192.168.1.20)

Every phone and tablet on the Wi-Fi can now open the name. Keep the address too. An agent that cannot resolve .local names will need it; see Let other agents speak house.

Port 4000 is the default. For a bare http://dobby.local/, run on port 80. On macOS, PORT=80 just works. On Linux, the installed service's unit grants the capability, so port: 80 in the house file is enough there; in development, put a reverse proxy in front or stay on 4000.

The trust is the Wi-Fi password. Anyone on the network is the household. There is no login, and /admin is a room, not a privilege. Do not turn on the LAN bind on a network you would not hand the Wi-Fi password to.

Dobby answers only to its own names: localhost, the hostname in the house file (dobby.local unless you changed it), PHX_HOST if set, and any LAN address typed as digits. Any other name gets a plain 400. That is what stops a web page somebody in the house opens from pointing its own domain at the box and driving /admin. If you reach Dobby by another name, such as one your router's DNS gives the box or a reverse proxy's, list it in DOBBY_ALLOWED_HOSTS, comma separated, and restart.

Running as an installed service

Dobby ships as a release: a tarball with its own Erlang inside, one per architecture, from the GitHub releases page. It runs as a systemd service under /opt/dobby, migrates the database before every start, and reads /opt/dobby/config/home.yaml and /opt/dobby/config/soul.md at boot. The HA token, the model key, DATABASE_URL, and SECRET_KEY_BASE live in /opt/dobby/config/dobby.env, which the service alone can read. It listens on this machine only unless system.lan is true. Dobby speaks plain HTTP on the LAN; put a TLS proxy in front before exposing it further.

The install, the reboot, and the upgrade were walked on 2026-09-04 on a Debian 12 virtual machine standing in for the box. The commands and what they printed are on the box page.