Configuration
Four layers, each with a clear job: environment variables, the git-backed store, Spring Boot overrides, and per-entity settings.
The four layers
| Layer | Lives in | Holds | Changes take effect |
|---|---|---|---|
| Environment variables | The container / process environment | Bootstrap secrets: database URL and credentials, the JWT signing key, where the configuration repository is. | On restart |
| Configuration repository | A git repo read by ConfigProvider |
Everything runtime: MQTT, CORS, feature flags, integration credentials, dashboard bindings. | Within one configSync interval, or immediately when edited in the UI |
| Spring Boot overrides | ./config/application.yml next to the JAR |
Deployment facts the API must not be able to rewrite — trusted proxies, LAN ranges, job toggles. | On restart |
| Per-entity configuration | The Configuration table, edited in the UI |
Settings attached to a single device, port, peripheral or zone — auto-off timeouts, voice aliases, device tokens. | Immediately |
The appConfigUpdate GraphQL mutation can rewrite arbitrary configuration keys at
runtime. Putting myhab.security.trustedProxies and lanCidrs there would
make the allowlist editable through the API it exists to protect. They stay in
application.yml.
Environment variables
| Variable | Required | Purpose |
|---|---|---|
GRAILS_ENV | yes | production, development or demo. Selects the environment block in application.yml. |
DB_URL | yes | JDBC URL, e.g. jdbc:postgresql://db:5432/myhab. ?TimeZone=UTC is appended automatically if absent. |
DB_USERNAME | yes | Database user (defaults to myhab outside production). |
DB_PASSWORD | yes | Database password. |
JWT_SECRET | yes | HS256 signing key for access tokens. Use a long random string; changing it invalidates every issued token. |
CFG_REPO_URI | yes | Configuration repository — an https:// remote or a file:// bare repo. |
CFG_USERNAME | no | Git username. Leave empty for file://. |
CFG_PASSWORD | no | Git password or access token. |
TZ | recommended | Set to UTC. See Installation. |
ANTHROPIC_API_KEY / OPENAI_API_KEY | no | Fallback for the voice assistant's LLM key when it is not in the configuration store. |
GOOGLE_TTS_API_KEY | no | Fallback for the neural TTS service-account JSON. |
The branch ConfigProvider reads defaults to prod in the production
environment and dev elsewhere; override it with the myhab.config.branch
property.
The configuration repository
One YAML document, versioned in git, cloned by the application at start and re-read by the
configSync job. It is also editable in the UI under
Settings → App configuration, which commits back to the repository.
MQTT
mqtt:
hostname: mosquitto
port: 1883
username: myhab
password: change-me
topics: myhab/# # subscription filterThe outbound topic prefix (myhab) is set separately in application.yml under
mqtt.topic.prefix.
CORS
Only needed when the client is served from a different origin than the API — for example during development, or if you host the PWA separately.
cors:
allowedOrigin:
- https://home.example.com
- http://localhost:10002UI bindings
These tell the dashboard which entities to wire its fixed tiles to. Only keys under
specialDevices., specialZones., ui., grafana. and
surveillance. are exposed to the browser — the rest of the store never leaves the server
for non-admin users.
specialZones:
int: { id: 1001 } # "Interior" quick tile
ext: { id: 1003 } # "Exterior" quick tile
parter: { id: 1001 } # ground floor
etaj: { id: 1007 } # upper floor
lan: { id: 1005 } # lawn irrigation
garden: { id: 1006 } # garden irrigation
specialDevices:
doorLockMain:
peripheral:
id: 1234 # the gate the dashboard card operates
ui:
meteo:
locationName: My TownTwo optional integrations are wired the same way — an embedded Grafana dashboard for long-range charts, and a link to whatever surveillance UI you run:
grafana:
url: https://grafana.example.com
dashboard:
solar: { id: abc123 }
temperature: { id: def456, panelId: 4 }
surveillance:
url: https://cameras.example.comGiven https://grafana.example.com/d/abc123/solar-plant, set
grafana.url to the origin and grafana.dashboard.solar.id to
abc123. Without them the Solar Reports page explains what is missing instead of
rendering a broken frame.
Each dashboard widget declares the configuration keys it cannot work without. Settings will not offer a widget whose keys are missing, and the dashboard renders a “not configured” placeholder — with an inline picker for admins — instead of mounting a broken card.
Heating
heat:
thermostat:
enabled: true
temp:
allDay: 21 # default setpoint in °CAuto-import
admin:
devices:
autoimport: false # create Device rows from unknown MQTT announcements
ports:
autoimport: false # create DevicePort rows from unknown port topicsUseful when bringing a new controller online; turn it back off afterwards so a misbehaving device cannot spawn hundreds of rows.
Notifications
telegram:
token: "<bot token>"
chanelId: "<channel id>"
bot1x1ChannelId: "<direct chat id>"
push:
vapid:
publicKey: "<vapid public key>"
privateKey: "<vapid private key>"
subject: "mailto:you@example.com"
opsgenie:
url: https://api.opsgenie.com
apiKey: "<key>"
team: "<team>"
recipientId: "<id>"
alias: myhab
email: alerts@example.comAll three degrade cleanly when absent: the Telegram bot stays off (it also has its own
myhab.telegram.enabled switch), web push logs one warning and stays inactive, and
OpsGenie keys are only read when an alert is actually raised.
Feature flags
| Key | Default | Purpose |
|---|---|---|
feature.voice.enabled | false | Master switch for the voice assistant. |
feature.voice.llm.provider | anthropic | anthropic or openai. |
feature.voice.llm.model | provider default | Model id override. |
feature.voice.llm.apikey | — | LLM key; falls back to the provider's environment variable. |
feature.voice.tts.enabled | false | Server-side neural speech for replies. |
feature.voice.tts.provider | google | Currently Google Cloud TTS only. |
feature.voice.tts.apikey | — | A service-account JSON (inline or a file path) — not an API key. |
feature.voice.tts.voice.ro / .en | Google default | Specific voice names, e.g. ro-RO-Wavenet-A. |
feature.qr.enabled | false | Embed QR codes on printed labels. |
feature.qr.content.template | myhab://TYPE/ID | What the QR encodes. Deliberately base-URL-free. |
feature.qr.position / feature.qr.size | — | Placement and size on the label. |
Quartz job intervals
Every scheduled job can be enabled, disabled and re-timed from
quartz.jobs.* without touching code. These live in application.yml and are
typically overridden per deployment in ./config/application.yml.
| Job | Default | Interval | Does |
|---|---|---|---|
deviceControllerStateSync | on | 60 s | HTTP reachability + state reconciliation for controllers that support it. |
portValueSyncTrigger | on | 60 s | Asks devices to re-publish their port values over MQTT. |
configSync | on | 60 s | Pulls the configuration repository. |
switchOffOnTimeout | on | 30 s | Turns off anything past its auto-off timeout. |
meteoStationSync | on | 1800 s | Open-Meteo current conditions and forecast. |
nibeInfoSync | on | 300 s | Heat pump telemetry. Inert without credentials. |
nibeTokenRefresh | on | 120 s | Refreshes the myUplink OAuth token before expiry. |
heatingControl | off | 120 s | Thermostat automation from room temperature and degree-minutes. |
huaweiInfoSync | off | 300 s | FusionSolar inverter and meter poll. |
navimowInfoSync | off | 30 s | Mower state; auto-creates ports and raises notifications. |
navimowTokenRefresh | off | 600 s | Spends the stored refresh token. Enable after the OAuth flow has run once. |
eventLogReader | off | 60 s | Drains the event log queue. |
electricityMetricStatistics1H / 24H / 1Month | off | 1 h / 24 h / 30 d | Energy statistics rollups. |
randomColors, rainbowRGB | off | 5 s / 80 s | RGB effects — testing and parties. |
# ./config/application.yml — enable the integrations you actually own
quartz:
jobs:
huaweiInfoSync: { enabled: true, interval: 300 }
heatingControl: { enabled: true, interval: 120 }
eventLogReader: { enabled: true, interval: 60 }
nibeInfoSync: { enabled: false }Security settings
myhab:
security:
# Reverse proxies whose X-Real-IP we believe. Must be the proxy's own address.
# Empty means no request arriving with forwarding headers is trusted.
trustedProxies: []
# Networks the anonymous device endpoints may be called from.
lanCidrs:
- 192.168.0.0/16
- 10.0.0.0/8
- 172.16.0.0/12
- 127.0.0.0/8
- ::1/128forward-headers-strategy must stay unset.
Enabling it rewrites request.remoteAddr from X-Forwarded-For, which is
caller-supplied. myHAB deliberately distinguishes the proxy from the caller itself via
ClientIpService.
Per-entity configuration
Many entities carry their own key/value sidecar, edited from that entity's screen rather than globally.
| Key | Attached to | Purpose |
|---|---|---|
key.on.timeout | Peripheral | Auto-off after N seconds. Also shown to guests on a shared link. |
feature.voice.alias | Peripheral, Zone | Comma-separated alternate names fed to the voice agent. |
cfg.key.device.http.sync.supported | Device | Include this controller in the HTTP state sweep. |
cfg.key.device.mqtt.sync.supported | Device | Include it in the MQTT value-read trigger. |
cfg.key.device.admin.port.autoimport | Device | Per-device port auto-import override. |
cfg.key.device.oauth.access_user / .access_passwd | Device | Credentials for a cloud-backed device. |
cfg.key.device.oauth.access_token / .refresh_token | Device | Tokens written by the OAuth flow and refreshed by a job. |
cfg.key.device.navimow.api.base_url | Device | Regional Segway API endpoint. |
cfg.key.device.navimow.device.id | Device | The mower id returned by the Segway account listing. |
Settings in the app
Some things are per-user rather than per-installation, and live in the profile:
- Language — English or Romanian, or Automatic to follow the browser. The same choice drives voice recognition and spoken replies.
- Timezone — an IANA id used to render every stored UTC timestamp.
- Dashboard widgets — which cards appear, in what order.
- Favourites — jobs and scenarios pinned for one-tap execution.
- Avatar — a small PNG/JPEG stored on the account.