Home Assistant + NATS = HATS
A bridge that streams Home Assistant events into NATS subjects and exposes a lightweight caching proxy for Home Assistant’s REST API.
Alpha software. I have been running this at home for over a year, but breaking changes can occur at any time.
HATS connects to Home Assistant via websocket and republishes state-change events onto NATS subjects in real time. Subscriber services connect to NATS and react to events — no polling, no direct Home Assistant dependency in your subscriber code. HATS also runs an HTTP API that caches entity state in a JetStream KV store, so state reads inside event handlers don’t hit Home Assistant on every event.
Home Assistant ──ws──▶ HATS ──pub──▶ NATS
│
└──────────▶ HTTP API (cached state, timers, schedules, commands)
▲
Subscriber ──┘
1. Run HATS alongside NATS (see example/compose.yaml for a fuller version):
services:
nats:
image: nats:2
command: -js # enable JetStream
ports:
- "4222:4222"
hats:
image: codeberg.org/jhot/hats:latest
ports:
- "8888:8888"
environment:
- HASS_HOST=192.168.1.100 # your Home Assistant IP
- HASS_TOKEN=your-ha-token # long-lived access token from HA profile
- NATS_HOST=nats
- HATS_TOKEN=changeme # bearer token for the HATS API
depends_on:
- nats
2. Write a subscriber:
package main
import (
"log/slog"
"os"
"os/signal"
"syscall"
"codeberg.org/jhot/hats/pkg/client"
"codeberg.org/jhot/hats/pkg/config"
"codeberg.org/jhot/hats/pkg/homeassistant"
"codeberg.org/jhot/hats/pkg/nats"
)
func main() {
cfg, _ := config.New()
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
hatsClient := client.NewHatsClient(cfg.GetHatsBaseUrl(), cfg.HatsToken)
natsClient := nats.New(
nats.WithoutJetstream,
nats.WithHostName(cfg.NatsHost),
nats.WithPort(cfg.NatsPort),
nats.WithClientName(cfg.NatsClientName),
nats.WithLogger(logger),
)
natsClient.Connect()
defer natsClient.Close()
// React when the sun rises
natsClient.AddStateSub(nats.StateTopicType, "sun.sun", func(state homeassistant.StateData) error {
if state.State == "above_horizon" {
if motionOn, _ := hatsClient.GetStateBool("binary_sensor.living_room_motion"); !motionOn {
return hatsClient.CallService("light.living_room", homeassistant.Services.TurnOff)
}
}
return nil
})
sigch := make(chan os.Signal, 1)
signal.Notify(sigch, syscall.SIGINT, syscall.SIGTERM)
<-sigch
}
See the subscriber guide for a full walkthrough.
| Subject | Fires when | Payload |
|---|---|---|
homeassistant.states.{domain}.{entity}.{state} |
Entity state changes | StateData |
homeassistant.attributes.{domain}.{entity}.{state} |
Attributes change, state unchanged | StateData |
homeassistant.zha.{device_ieee} |
ZHA device event | ZHA event data |
homeassistant.zwave-scene.{device_id} |
Z-Wave JS scene event | ZwaveJS value notification |
homeassistant.nfc.{tag_id} |
NFC tag scanned | NFC tag data |
homeassistant.timers.{name}.finished |
HATS timer expired | "finished" |
schedules.{name} |
HATS cron schedule fired | "finished" |
command.{name} |
HTTP POST to /api/command/{name} |
Raw POST body bytes |
See docs/nats-topics.md for full payload shapes and NATS CLI examples.
| Variable | Default | Description |
|---|---|---|
HASS_HOST |
127.0.0.1 |
Home Assistant hostname or IP |
HASS_TOKEN |
(required) | Long-lived access token from HA profile |
NATS_HOST |
127.0.0.1 |
NATS server hostname |
NATS_PORT |
4222 |
NATS server port |
HATS_TOKEN |
— | Bearer token for the HATS API (optional) |
LOG_LEVEL |
INFO |
DEBUG, INFO, WARN, or ERROR |
See docs/getting-started.md for the full variable reference.
AddStateSub, AddEventSub, AddSub, AddRawSub)