Getting Started¶
Build your first Home Assistant automation as a standalone Rust process.
Prerequisites¶
| Requirement | Why |
|---|---|
| Rust (stable) | rustup recommended — workspace builds with cargo |
| Home Assistant | Long-lived access token (how to create one) |
| Linux + systemd | Production supervisor — one service per automation |
| Python 3.10+ | Only if using Python bindings |
Building the workspace¶
All crates build from the workspace root. Binaries land in target/release/.
1. Create an automation¶
Each automation is its own binary crate. Create one alongside the library:
Add dependencies to automations/my-automation/Cargo.toml:
[dependencies]
signal-ha = { path = "../../crates/signal-ha" }
tokio = { version = "1", features = ["full"] }
serde_json = "1"
anyhow = "1"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
2. Minimal example — motion-activated light¶
use signal_ha::HaClient;
use std::env;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt::init();
let url = env::var("HA_WS_URL")?; // ws://ha:8123/api/websocket
let token = env::var("HA_TOKEN")?;
let client = HaClient::connect(&url, &token).await?;
// Subscribe to a single entity
let mut rx = client.subscribe_state("binary_sensor.hallway_motion").await?;
while let Ok(change) = rx.recv().await {
if let Some(ref new) = change.new {
if new.state == "on" {
client.call_service("light", "turn_on", serde_json::json!({
"entity_id": "light.hallway",
"brightness": 200
})).await?;
} else {
client.call_service("light", "turn_off", serde_json::json!({
"entity_id": "light.hallway"
})).await?;
}
}
}
Ok(())
}
Key points:
subscribe_state(entity_id)returns abroadcast::Receiver<StateChange>- Each
StateChangehasentity_id,old: Option<EntityState>,new: Option<EntityState> EntityStatecontainsstate(string),attributes(JSON), andlast_changed(UTC)
3. Add a status page¶
Every automation should expose a status page for observability:
use signal_ha::StatusPage;
let status = StatusPage::new("my-automation", 9100);
status.spawn(); // starts HTTP server in background
// Update from your main loop
status.set_bool("State", "Motion detected", true);
status.set("State", "Last trigger", "2 min ago");
status.set_enum("Mode", "Current", "auto", &["auto", "manual", "off"]);
Browse to http://host:9100 to see a live dashboard that auto-refreshes every 5 seconds.
4. Add sun-aware scheduling¶
use signal_ha::Scheduler;
use tokio_stream::StreamExt;
let sched = Scheduler::new(51.5, -0.1); // London
// Fire 30 min before sunset every day
let mut sunset_stream = sched.at_sunset(chrono::Duration::minutes(-30));
while let Some(time) = sunset_stream.next().await {
tracing::info!("Pre-sunset trigger at {time}");
// turn on outdoor lights...
}
Other scheduling methods: at_sunrise(), daily(), after(), is_sun_up().
5. Add a dashboard¶
Ship a dashboard.yaml alongside your automation to auto-create a Lovelace dashboard in HA:
url_path: signal-my-automation
title: "My Automation"
icon: "mdi:lightbulb"
config:
views:
- title: Status
cards:
- type: entities
entities:
- light.hallway
- binary_sensor.hallway_motion
Load and sync it at startup:
use signal_ha::DashboardSpec;
let spec = DashboardSpec::from_yaml(include_str!("../dashboard.yaml"))?;
spec.ensure(&client).await?;
6. Deploy with systemd¶
[Unit]
Description=My Automation (signal-ha)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/local/bin/my-automation
Environment=HA_WS_URL=ws://homeassistant.local:8123/api/websocket
Environment=HA_TOKEN=your_long_lived_token
Restart=on-failure
RestartSec=10
NoNewPrivileges=true
ProtectSystem=strict
[Install]
WantedBy=multi-user.target
7. Add an LLM observation agent (optional)¶
Embed an agent that periodically reviews your automation's behaviour and posts findings:
use signal_ha_agent::{AgentConfig, AgentHandle};
use signal_ha_agent::ha_host::HaHost;
use std::sync::Arc;
use std::time::Duration;
let ha_host = Arc::new(
HaHost::new(client.clone(), &url, &token, "my-automation")
.with_board_url("http://localhost:9200")
);
let agent = AgentHandle::spawn(AgentConfig {
name: "my-automation".into(),
role: "You are the hallway automation specialist.".into(),
description: "Turns on hallway light when motion is detected.".into(),
ha_client: client.clone(),
conversation_entity: None, // auto-detect
primary_entities: vec![
"light.hallway".into(),
"binary_sensor.hallway_motion".into(),
],
area: Some("hallway".into()),
max_iterations: 8,
default_interval: Duration::from_secs(86400), // daily
ha_host,
memory_path: "/var/lib/signal-ha/my-automation/memory.json".into(),
disallowed_calls: vec![],
transcript_dir: Some("/var/lib/signal-ha/transcripts".into()),
inject_current_time: true,
dashboard_url_path: Some("signal-my-automation".into()),
});
Trigger the agent manually:
The agent writes findings to the message board and updates an agent summary entity on your dashboard.
Next steps¶
- Architecture — how the crates fit together
- signal-ha — core library API reference
- signal-ha-lighting — lighting primitives (actuators, overlays, lux curves)
- signal-ha-agent — agent configuration and tools
- message-board — findings API for agent collaboration