Skip to main content

Configuration Reference

HomeCore is configured with a single TOML file. By default it looks for config/homecore.toml relative to the base directory (current working directory, or --home / HOMECORE_HOME).

# Config resolution order:
# 1. --config /path/to/file
# 2. HOMECORE_CONFIG env var
# 3. {base_dir}/config/homecore.toml

Full annotated example

# ── Server ────────────────────────────────────────────────────────────────────
[server]
host = "0.0.0.0"
port = 8080

# IP addresses or CIDR ranges that bypass JWT authentication entirely.
# Requests from these IPs are granted Admin-level access without a token.
# Useful for trusted LAN clients (dashboards, scripts on localhost).
# When a Bearer token IS present, JWT validation always runs regardless of whitelist.
whitelist = ["127.0.0.1", "192.168.1.0/24"]

# ── MQTT Broker ───────────────────────────────────────────────────────────────
[broker]
host = "0.0.0.0"
port = 1883

# Optional TLS listener (runs alongside plain-text port)
# tls_port = 8883
# cert_path = "/etc/homecore/broker.crt"
# key_path = "/etc/homecore/broker.key"

# Use an external broker (e.g. Mosquitto) instead of the embedded rumqttd.
# When set, HomeCore connects as a client and skips its own listener bind.
# Required for topic-level authz enforcement — see Administration → Broker
# for the rumqttd-vs-Mosquitto split and `hc-cli broker generate-mosquitto-config`.
# external_url = "mqtt://mosquitto.local:1883"

# When [[broker.clients]] entries are present, the embedded broker requires
# credentials on CONNECT. The `allow_pub` / `allow_sub` patterns are
# metadata-only on rumqttd — they are NOT enforced at publish/subscribe time.
# For per-topic enforcement, deploy with external Mosquitto and run
# `hc-cli broker generate-mosquitto-config` to convert these patterns into
# Mosquitto's ACL file.
[[broker.clients]]
id = "internal.core"
password = "a-strong-random-password"
allow_pub = ["homecore/#"]
allow_sub = ["homecore/#"]

[[broker.clients]]
id = "plugin.hue"
password = "hue-plugin-password"
allow_pub = ["homecore/devices/hue_+/state", "homecore/plugins/hue/+"]
allow_sub = ["homecore/devices/hue_+/cmd"]

# ── Authentication ────────────────────────────────────────────────────────────
[auth]
# JWT signing secret. Change this! Use a long random string.
# If not set, a random secret is generated each startup (tokens expire on restart).
jwt_secret = "change-this-to-a-long-random-string"
token_expiry_hours = 24

# ── Location (required for solar triggers) ───────────────────────────────────
[location]
latitude = 38.9072 # Washington D.C. defaults
longitude = -77.0369
timezone = "America/New_York"

# ── Storage ───────────────────────────────────────────────────────────────────
# Paths are relative to base_dir unless absolute.
[storage]
state_db_path = "data/state.redb" # device registry, rules, users, scenes
history_db_path = "data/history.db" # time-series attribute history

# ── Rules ─────────────────────────────────────────────────────────────────────
[rules]
# Directory containing rule RON files. Hot-reloaded on any file change.
dir = "rules"

# ── Scheduler ─────────────────────────────────────────────────────────────────
[scheduler]
# On restart, fire time-based triggers that occurred within this window.
# Set 0 to disable catch-up entirely.
catchup_window_minutes = 15

# ── Startup ───────────────────────────────────────────────────────────────────
[startup]
# Seconds to wait after startup before mode manager publishes initial modes.
# Gives plugins time to connect and subscribe before receiving cmd messages.
plugin_ready_delay_secs = 10

# ── Modes ─────────────────────────────────────────────────────────────────────
[modes]
# Path to modes.toml file. Hot-reloaded on change.
path = "config/modes.toml"

# ── Calendar ──────────────────────────────────────────────────────────────────
# [calendar]
# dir = "calendars" # directory of .ics files; hot-reloaded
# expansion_days = 400 # how far ahead to expand recurring events

# ── Notifications ─────────────────────────────────────────────────────────────
[notify]
[[notify.channels]]
name = "telegram"
type = "telegram"
bot_token = "123456789:ABCDEFxxxxxxxxxxxxxxxxxxxxxxx"
chat_id = "-1001234567890"

[[notify.channels]]
name = "pushover"
type = "pushover"
api_key = "your-pushover-app-key"
user_key = "your-pushover-user-key"

[[notify.channels]]
name = "email-alerts"
type = "email"
from = "homecore@yourdomain.com"
to = ["you@yourdomain.com"]
[notify.channels.smtp]
host = "smtp.yourdomain.com"
port = 587
username = "homecore@yourdomain.com"
password = "smtp-password"
starttls = true

# ── Logging ───────────────────────────────────────────────────────────────────
[logging]
level = "info"
time_display = "local" # "local" | "utc"

[logging.targets]
hc_core = "info"
hc_api = "info"
hc_broker = "warn"
hc_mqtt_client = "info"

[logging.stderr]
enabled = true
format = "pretty" # "pretty" | "compact" | "json"
ansi = true

[logging.file]
enabled = false
dir = "logs"
prefix = "homecore"
rotation = "daily" # "daily" | "hourly" | "weekly" | "never"
max_size_mb = 100
compress = true
format = "json"
prune_after_days = 30 # delete rotated files older than N days; 0 = never prune

# Per-plugin tracing logs forwarded to the broker on
# `homecore/plugins/<id>/logs` are merged into core's /logs/stream by
# the StateBridge. Default forwarding level is "info" — bump to "debug"
# for a single misbehaving plugin via the management API or here.
[logging.plugin_forward]
default_level = "info" # trace | debug | info | warn | error

# [logging.syslog]
# enabled = false
# transport = "udp" # "udp" | "tcp"
# host = "192.168.1.100"
# port = 514
# protocol = "rfc5424" # "rfc5424" | "rfc3164"
# facility = "daemon"
# app_name = "homecore"

# ── Web Admin UI ──────────────────────────────────────────────────────────────
[web_admin]
enabled = false # serve pre-built Leptos/WASM admin UI
# dist_path = "ui/dist" # path to trunk build output, relative to home dir

# ── Engine ────────────────────────────────────────────────────────────────────
[engine]
drain_timeout_secs = 10 # time to wait for in-flight rule tasks on shutdown
fire_history_limit = 500 # max evaluation records per rule

# ── Plugins (managed) ────────────────────────────────────────────────────────
# Each [[plugins]] entry defines a managed plugin that HomeCore supervises.
# [[plugins]]
# id = "plugin.hue"
# binary = "plugins/hc-hue/bin/hc-hue" # relative to HOMECORE_HOME
# config = "plugins/hc-hue/config/config.toml"
# enabled = true

# [[plugins]]
# id = "plugin.wled"
# binary = "plugins/hc-wled/bin/hc-wled"
# config = "plugins/hc-wled/config/config.toml"
# enabled = true

# ── Ecosystem profiles ────────────────────────────────────────────────────────
# [ecosystem]
# profiles_dir = "config/profiles" # directory of .toml profile files

Section reference

[server]

KeyTypeDefaultDescription
hoststring"0.0.0.0"Bind address for the HTTP/WebSocket API
portinteger8080Listen port

[broker]

KeyTypeDefaultDescription
hoststring"0.0.0.0"Embedded-broker bind address (ignored when external_url is set)
portinteger1883Plain-text MQTT v3 port
v5_portinteger1884Plain-text MQTT v5 port. Set to null to disable.
tls_portintegerTLS MQTT port (requires cert_path and key_path)
cert_pathstringPath to TLS certificate file (PEM)
key_pathstringPath to TLS private key file (PEM)
external_urlstringConnect to an external broker (e.g. mqtt://mosquitto:1883) instead of running the embedded one. Required for per-topic ACL enforcement; see Administration → Broker.

[[broker.clients]] entries:

KeyTypeDescription
idstringClient ID (used as MQTT username)
passwordstringPlain-text password (hashed internally)
allow_pubarray of stringsAllowed publish topic patterns (MQTT wildcards +/# supported)
allow_subarray of stringsAllowed subscribe topic patterns

Important: The embedded broker enforces connection-level credentials but does not enforce per-topic ACL. allow_pub/allow_sub are metadata only. For strict topic ACL, deploy against an external Mosquitto broker — see the broker deployment guide. hc-cli broker generate-mosquitto-config converts these patterns to a Mosquitto ACL file automatically.

[storage]

KeyTypeDefaultDescription
state_db_pathstring"data/state.redb"Path to the redb state database (devices, rules, users, dashboards, audit, battery latches). Relative to HOMECORE_HOME.
history_db_pathstring"data/history.db"Path to the SQLite time-series history DB.

[profiles]

KeyTypeDefaultDescription
dirstring"config/profiles"Directory of ecosystem profile TOML files (Tasmota, Shelly, Zigbee2MQTT, etc.) consumed by the topic mapper.

[rules]

KeyTypeDefaultDescription
dirstring"rules"Directory of .ron rule files. Hot-reloaded on change.

[auth]

KeyTypeDefaultDescription
jwt_secretstringunsetDeprecated. Inline HS256 secret. If set, overrides jwt_secret_file and emits a warning. Prefer the file-managed default.
jwt_secret_filestring<parent-of-state_db_path>/jwt_secretPath to a 0600 file holding the persistent JWT secret. Auto-generated on first startup so issued tokens survive restarts.
token_expiry_hoursinteger24Access JWT lifetime in hours.
refresh_token_expiry_daysinteger30Refresh token lifetime. Each /auth/refresh rotates the token; reuse triggers full chain revocation.
audit_retention_daysinteger365How many days of audit history to keep. A background task prunes older entries every 6 hours.
whitelistarray of strings[]Deprecated. IP addresses or CIDR ranges that bypass JWT auth and receive Admin access. Both IPv4 and IPv6 supported (e.g. ["127.0.0.1/32", "::1/128"]). Prefer [auth.admin_uds] for same-host tooling.

[auth.admin_uds] — Unix domain socket listener for same-host admin tooling (replaces whitelist):

KeyTypeDefaultDescription
enabledbooltrueListen on the admin UDS.
pathstring/run/homecore/admin.sockSocket path.
modeinteger (octal)0o660Filesystem permissions on the socket.
allowed_uidsarray of integers[]UIDs allowed to connect. Empty = only the homecore service UID, resolved at startup.

[location]

Required for SunEvent and SunEvent offset triggers.

KeyTypeDescription
latitudefloatDecimal degrees, e.g. 38.9072
longitudefloatDecimal degrees, e.g. -77.0369
timezonestringIANA timezone name, e.g. "America/New_York"

[battery]

Drives the battery alert watcher. The watcher synthesizes device_battery_low and device_battery_recovered events from device state changes, with hysteresis enforced in core (latches at threshold_pct, clears at threshold_pct + recover_band_pct). See Battery monitoring for the full picture.

KeyTypeDefaultDescription
threshold_pctfloat20.0Battery percentage at or below which the latch engages.
recover_band_pctfloat5.0Recovery band added to threshold to clear the latch. Recovery fires at threshold_pct + recover_band_pct.
notify_channelstringunsetOptional hc-notify channel name. When set, the watcher sends a built-in notification on each low edge — no rule required.
notify_on_recoveredboolfalseWhen true and notify_channel is set, recovery edges also notify.
[battery]
threshold_pct = 20.0
recover_band_pct = 5.0
# notify_channel = "all"
# notify_on_recovered = false

[scheduler]

KeyTypeDefaultDescription
catchup_window_minutesinteger15On restart, fire missed time-based triggers that occurred within this many minutes. Set 0 to disable.

[startup]

KeyTypeDefaultDescription
plugin_ready_delay_secsinteger10Grace period before mode manager publishes initial states. Prevents command-before-subscribe race with plugins.

[shutdown]

KeyTypeDefaultDescription
drain_timeout_secsinteger10Seconds to wait for in-flight rule action tasks to finish on graceful shutdown before force-stopping.

[logging]

Top-level keys:

KeyTypeDefaultDescription
levelstring"info"Global log level (error, warn, info, debug, trace).
targetstable of string→string{}Per-target overrides keyed by Rust module path with underscores (e.g. hc_core = "debug"). Equivalent to RUST_LOG directives.
time_displaystring"local"Timestamp display: "local" or "utc".

[logging.stderr]:

KeyTypeDefaultDescription
enabledbooltrueEmit to stderr.
formatstring"pretty""pretty", "compact", or "json".
ansibooltrueANSI color codes (set false when piping to systemd journal).

[logging.file] — rolling file output:

KeyTypeDefaultDescription
enabledbooltrueEnable rolling file output.
dirstring"logs"Log directory (created if missing).
prefixstring"homecore"Filename prefix; rotated files become <prefix>.YYYY-MM-DD.
rotationstring"daily""daily", "hourly", "weekly", or "never".
max_size_mbinteger100Max size before rotation. Combined with rotation ("whichever first"); 0 disables size-based rotation.
compressbooltrueGzip rotated files (background thread).
prune_after_daysinteger0Delete rotated files older than N days. 0 = never prune.
formatstring"json""json", "compact", or "pretty".

[logging.rules_file] — separate rule-engine log capturing only hc_core at debug regardless of the global level. Disabled by default. Same field set as [logging.file] plus its own defaults (prefix "rules", format "pretty").

[logging.stream] — live /api/v1/logs/stream WebSocket:

KeyTypeDefaultDescription
enabledbooltrueEnable the streaming endpoint.
ring_buffer_sizeinteger500Recent log lines retained for new subscribers.

[logging.syslog] — RFC 5424 syslog forwarding:

KeyTypeDefaultDescription
enabledboolfalseEnable syslog output.
transportstring"udp""udp" (recommended) or "tcp".
hoststring"127.0.0.1"Syslog server.
portinteger514Syslog port.

[notify]

Channels are declared as a list under [[notify.channels]]. The name is referenced from rule Notify actions. The reserved name "all" fans out to every registered channel.

[[notify.channels]]
name = "primary_email"
type = "email"
smtp_host = "smtp.example.com"
smtp_port = 587
username = "automation@example.com"
password = "..."
from = "automation@example.com"
to = ["alerts@example.com"]

[[notify.channels]]
name = "phone_push"
type = "pushover"
api_token = "..."
user_key = "..."

[[notify.channels]]
name = "telegram"
type = "telegram"
bot_token = "..."
chat_id = "..."

Built-in providers: email (SMTP), pushover, telegram. Channels that fail to initialise at startup are logged and skipped — they don't block the rest of the system.

[web_admin]

KeyTypeDefaultDescription
enabledbooleanfalseServe the pre-built Leptos/WASM admin UI as static files via tower-http ServeDir
dist_pathstring"ui/dist"Path to the trunk build output directory, relative to HOMECORE_HOME

When enabled, HomeCore serves the Leptos admin UI at the root path. API routes at /api/v1 take priority over static file serving. A SPA fallback returns index.html for any unmatched path, enabling client-side routing. Disabled by default so that during development you can use trunk serve separately.

See Web UI overview for what the admin client provides.

[calendars]

KeyTypeDefaultDescription
dirstring"config/calendars"Directory of .ics calendar files. Hot-reloaded on file changes.
expansion_daysinteger400How many days forward to expand recurring events into individual occurrences.

Glue devices

The path to glue device definitions is fixed at <base>/config/glue.toml and is not configurable from homecore.toml. See Virtual / glue devices for the file format (timers, switches, counters, modes).

[[plugins]]

Each entry defines a managed plugin that HomeCore supervises. Managed plugins support heartbeat monitoring, start/stop/restart, and remote configuration.

KeyTypeDescription
idstringPlugin ID (matches the plugin's plugin_id config)
binarystringPath to the plugin binary (relative to HOMECORE_HOME)
configstringPath to the plugin config file (relative to HOMECORE_HOME)
enabledbooleanWhether the plugin should be started automatically

modes.toml reference

The modes configuration is a separate file (config/modes.toml), hot-reloaded when changed.

# Solar modes — computed from sunrise/sunset with optional offset
[[modes]]
name = "mode_night"
type = "solar"
# on_at_offset = 0 # minutes after sunset (negative = before)
# off_at_offset = 0 # minutes after sunrise (negative = before)

# Manual boolean modes — toggled via API or rule actions
[[modes]]
name = "mode_away"
type = "manual"
default = false

[[modes]]
name = "mode_vacation"
type = "manual"
default = false

[[modes]]
name = "mode_movie"
type = "manual"
default = false

Solar mode behavior:

  • mode_night is true from sunset to sunrise, false during daylight hours
  • Offsets shift the transition point: on_at_offset = -30 turns night mode on 30 minutes before sunset
  • Mode state is republished via MQTT as DeviceStateChanged at every transition

Mode API:

# Get all mode states
curl -s http://localhost:8080/api/v1/modes -H "Authorization: Bearer $TOKEN" | jq

# Set a manual mode on
curl -s -X PATCH http://localhost:8080/api/v1/modes/mode_away \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"on": true}'

# Adjust solar offset (minutes relative to sunset/sunrise)
curl -s -X PATCH http://localhost:8080/api/v1/modes/mode_night \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"on_at_offset": -30}'

Environment variable overrides

Some sensitive values can be set via environment variables instead of the config file:

HOMECORE_JWT_SECRET="your-secret"   \
HOMECORE_LAT="38.9072" \
HOMECORE_LON="-77.0369" \
HOMECORE_TZ="America/New_York" \
./bin/homecore

These are particularly useful in Docker deployments where secrets should not be baked into config files.