Configuration and CLI reference¶
For pool owners and operators running a release build of Aqualink Automate. This is the authoritative reference for every command-line flag and config-file key. Building from source lives in INSTALL.md.
Aqualink Automate is configured entirely at startup, through command-line flags and an optional configuration file. There is no runtime settings file that the app writes back to; what you pass at launch is what it runs with.
This page documents:
- How options are provided
- Config file format and example
- The 13 option areas, one section each
- Non-obvious behaviors and gotchas
- Conflict and dependency matrix
- Worked examples
How options are provided¶
You can set every option two ways: on the command line, or as a key in a configuration file. The two are merged at startup.
Command line vs config file¶
- Command line: options use their long name with a leading
--(for example--mqtt-host 192.168.1.100). Six options also have a short form:-c -d -h -v -s -r. - Config file: select a file with
--config <path>(or-c <path>). Keys are the option long name without dashes (for examplemqtt-host, not--mqtt-host). This long-name-equals-config-key equivalence holds for every option in this document.
Precedence: the command line wins¶
Command-line arguments are parsed first; the config file is read second and merged into the same option set. Merging is first-write-wins — a value already set on the command line is never overwritten by the config file. So if an option appears in both places, the command-line value is used.
Flat-INI config format¶
The config file is a flat INI file: one key = value per line, no [sections]. See Config file format and example for the exact rules.
Flag truthiness¶
Some options are flags — they take no value on the command line (for example --debug, --mqtt, --disable-https). In a config file, write them as key = <truthy-or-falsy>:
- Truthy (
true,yes,on,1, case-insensitive) enables the flag. - Anything else (including
false,no,0, or an empty value) omits the flag entirely, leaving it at its default.
Non-flag options always take a value in the config file (mqtt-host = 192.168.1.100).
Unknown arguments vs unknown keys¶
The two input paths treat unrecognized entries differently:
- An unknown command-line argument is fatal. The app logs the unrecognized argument and exits without starting.
- An unknown config-file key only produces a warning. The app logs
Config file: unrecognized option '<key>'and continues. This means a typo in a config key silently has no effect — check the log if a setting does not take.
Important: If you pass --config <path> but the file does not exist, parsing fails and the app exits. A missing --config flag is fine (the app just runs with defaults and command-line flags).
Inspecting the live option set¶
Every registered option, with its long name, short name, and description, is exposed at runtime as JSON at GET /api/diagnostics/options. Use it to confirm exactly which options a given build accepts.
Config file format and example¶
Rules, as parsed:
- One
key = valuepair per line. Whitespace around the key and value is trimmed. - Blank lines are ignored.
- Lines beginning with
#are comments and are ignored. - A line with no
=, or with an empty key, is skipped with a warning. - Keys are option long names without the leading dashes. There are no sections.
- Flag keys follow the truthiness rule above.
Four ready-to-copy example files ship in the repo. Start from the one that matches your wiring, or from the complete reference if you want to see every option in one place:
| File | Use case |
|---|---|
| examples/config-full.conf | Complete reference — every option, grouped and commented with its default. Copy it and uncomment what you need. |
| examples/config-serial.conf | A physical RS-485 serial port on the host. |
| examples/config-network.conf | A remote serial port over a serial-to-Ethernet adapter (RFC2217). |
| examples/config-docker.conf | Running inside a Docker container (includes Matter and host-networking notes). |
A minimal physical-serial config looks like this:
# config.conf — physical RS-485 on the host
serial-port = /dev/ttyUSB0
baudrate = 9600
jandy-device-type = onetouch
jandy-device-id = 0x41
address = 0.0.0.0 # bind the web UI on all interfaces (see the security note below)
disable-https = true # serve plain HTTP only
Run it with:
App options¶
General application options. Lives in the Application settings area.
| Option | Short | Type | Default | Description |
|---|---|---|---|---|
--config |
-c |
string | none | Path to the configuration file. If supplied and missing, startup fails. |
--debug |
-d |
flag | off | Enable debug-level logging. Conflicts with --trace. |
--trace |
flag | off | Enable trace-level logging (more verbose than debug). Conflicts with --debug. |
|
--help |
-h |
flag | off | Print help and exit. |
--version |
-v |
flag | off | Print the version and exit. |
--version-detail |
flag | off | Print detailed version info, including the git commit, and exit. |
--help, --version, and --version-detail print and exit immediately. They are checked before option validation, so --help still works even if the rest of the command line has a conflict (for example --debug --trace --help).
Serial and connectivity options¶
How the app reaches the RS-485 bus — a physical port or a network bridge. Lives in the Serial settings area.
| Option | Short | Type | Default | Description |
|---|---|---|---|---|
--serial-port |
-s |
string | platform-specific (see below) | Physical serial port for the Aqualink connection. |
--baudrate |
uint16 | 9600 |
Serial port baud rate. | |
--remote-serial-port |
-r |
string | none | Remote serial port as host:port (a serial-to-Ethernet bridge). |
--rfc2217 |
flag | on | Use the RFC2217 transport on the remote port (the default for remote ports). | |
--no-rfc2217 |
flag | off | Disable RFC2217 on the remote port; use a plain socket instead. | |
--rawtcp |
flag | off | Use raw TCP for the remote port. |
Default serial port is platform-specific: /dev/ttyS0 on Linux/macOS, COM1 on Windows.
Use a physical port unless --remote-serial-port is set; any non-empty --remote-serial-port switches the app to the network transport. RFC2217 is on by default for remote ports — pass --no-rfc2217 for a plain socket, or --rawtcp for raw TCP.
Note: The physical serial line is fixed at 9600 baud, no parity, 8 data bits, 1 stop bit, no flow control (9600 8N1). --baudrate exists for completeness; standard Aqualink hardware runs at 9600.
Conflicts (each pair is mutually exclusive): --serial-port vs --remote-serial-port; --rfc2217 vs --rawtcp; --rfc2217 vs --no-rfc2217; --no-rfc2217 vs --rawtcp.
Web server and security options¶
The built-in HTTP/HTTPS server that hosts the web UI and the REST/WebSocket API. Lives in the Web settings area.
| Option | Short | Type | Default | Description |
|---|---|---|---|---|
--address |
string | 127.0.0.1 |
Network interface to bind. Use 0.0.0.0 to expose on all interfaces. |
|
--http-port |
uint16 | 80 |
HTTP listen port. | |
--https-port |
uint16 | 443 |
HTTPS listen port. | |
--disable-content |
flag | off | Disable serving rendered content; expose APIs only. | |
--disable-http |
flag | off | Disable the HTTP server. | |
--disable-https |
flag | off | Disable the HTTPS server. | |
--cert |
string | exe-relative ssl/cert.pem |
TLS certificate (PEM). If absent at the default path, a unique self-signed cert is generated on first boot. | |
--cert-key |
string | exe-relative ssl/key.pem |
TLS certificate private key (PEM). Auto-generated (key written 0600) when absent at the default path; no key is shipped in the package. |
|
--cachain-cert |
string | none | CA chain certificate (PEM). | |
--doc-root |
string | exe-relative web asset dir |
Directory from which the web UI is served. | |
--api-auth-token |
string | none (no auth) | Require a bearer token on every API request and the WebSocket upgrade. Use a long random value (32+ chars); a token under 16 chars triggers a startup warning. | |
--api-allowed-origin |
string (repeatable) | none (check off) | Allowed Origin for the API/WebSocket. When set, a request/upgrade with a non-listed Origin is rejected with HTTP 403. |
|
--api-require-csrf-header |
flag | off | Require the X-Requested-With header on state-changing API requests (CSRF mitigation). |
|
--insecure-no-auth |
flag | off | Acknowledge an intentionally unauthenticated API on a non-loopback bind; downgrades the open-control-plane startup warning to an informational note. |
Security: --address defaults to 127.0.0.1 (loopback only) so a fresh install is not exposed to the network. Set --address 0.0.0.0 deliberately when you want LAN access.
Default paths are resolved at runtime, relative to the executable, not printed as a literal string:
--cert,--cert-key, and--doc-rootdefault to assets bundled next to (or near) the installed binary.--http-portand--https-porthave no fixed boost default; the app falls back to 80 and 443 respectively when you do not set them.
API authentication is off by default (--api-auth-token unset). When you set a token, every API request and the WebSocket upgrade must carry Authorization: Bearer <token> or the server answers HTTP 401. See usage-and-api.md for how the token is sent on the wire.
--cert and --cert-key are mutually required: set both or neither.
Conflicts: --disable-http vs --http-port; --disable-https vs each of --https-port, --cert, --cert-key, --cachain-cert. Because the conflict checker ignores defaulted options, --disable-https is accepted even though --cert/--cert-key carry default values — only an explicit --cert collides with it.
Authentication and identity options¶
The opt-in identity system — subjects, entitlements, and the policy engine (see SECURITY.md for the enforcement model and auth-redesign.md for the design). Lives in the Authentication settings area. Slice 1 ships the framework only: login and user management arrive in a later slice, so the app cannot yet issue tokens itself.
| Option | Short | Type | Default | Description |
|---|---|---|---|---|
--auth-mode |
enum | disabled |
Identity-system posture: disabled (historical behaviour — no identity resolution, every policy decision is Permit) or enabled (every request resolves to a subject and routes are gated by entitlements; anonymous callers get the deny-by-default Guest group). When enabled, the legacy --api-auth-token shared-token check is superseded — bearer credentials are interpreted by the subject resolver instead. |
|
--auth-state-dir |
string | none (platform secure state dir) | Directory for authentication state (JWT signing keys and the user/group/API-key/session/kiosk stores). When unset, an auth/ subdirectory of the platform's secure state directory is used (the same fallback chain as the generated TLS key). The directory is prepared owner-only; startup fails if no usable secure directory can be prepared. |
|
--jwt-access-ttl |
uint32 | 15 |
Access-token lifetime in minutes. Must be at least 1. | |
--bootstrap-admin |
string | none | Username of a first administrator to create at startup. Only fires when --auth-mode enabled and the user store is empty; idempotent (a no-op once any user exists). The password comes from --bootstrap-admin-password-file or AQUALINK_BOOTSTRAP_ADMIN_PASSWORD — never a CLI argument. |
|
--bootstrap-admin-password-file |
string | none | File whose first line (newline-terminated) is the bootstrap administrator's password. Used only with --bootstrap-admin. |
An --auth-mode value other than disabled/enabled, or a --jwt-access-ttl of 0, fails validation and stops startup.
Headless first-admin bootstrap. --bootstrap-admin <username> creates the first administrator without the interactive setup screen — useful for containers and unattended installs. It only acts when --auth-mode enabled and no users exist yet; with any user already on file it is silently a no-op (so a stale flag can never mint a second admin). The password is never passed on the command line, where it would be visible in the process table. Instead it is read from the first line of the file named by --bootstrap-admin-password-file, or, if that is unset, from the AQUALINK_BOOTSTRAP_ADMIN_PASSWORD environment variable. If --bootstrap-admin is given but no password can be found from either source, no administrator is created and a startup warning is logged. The password must satisfy the same policy as everywhere else (at least 12 characters).
MQTT and Home Assistant options¶
Publishing pool state to an MQTT broker, optionally with Home Assistant auto-discovery. Lives in the MQTT settings area. This is a summary — for the full topic layout, entity types, and discovery payloads, see the MQTT and Home Assistant guide.
| Option | Short | Type | Default | Description |
|---|---|---|---|---|
--mqtt |
flag | off | Enable the MQTT client. | |
--mqtt-protocol-version |
enum | 3.1.1 |
MQTT wire-protocol version the client speaks: 3.1.1 or 5.0. |
|
--mqtt-host |
string | localhost |
Broker hostname or IP. | |
--mqtt-port |
uint16 | 1883 |
Broker port (auto-switches to 8883 with --mqtt-tls if you do not set a port). |
|
--mqtt-tls |
flag | off | Enable TLS for the broker connection. | |
--mqtt-ca-cert |
string | none | TLS CA certificate path. | |
--mqtt-client-cert |
string | none | TLS client certificate path. Requires --mqtt-client-key. |
|
--mqtt-client-key |
string | none | TLS client key path. Requires --mqtt-client-cert. |
|
--mqtt-tls-skip-verify |
flag | off | Skip TLS certificate verification. | |
--mqtt-client-id |
string | none (auto-generated) | MQTT client identifier. | |
--mqtt-username |
string | none | Broker username. | |
--mqtt-password |
string | none | Broker password. | |
--mqtt-prefix |
string | aqualink |
Base topic prefix for all published topics. | |
--mqtt-status-interval |
uint32 | 5 |
Status publish interval (seconds). | |
--mqtt-stats-interval |
uint32 | 10 |
Statistics publish interval (seconds). | |
--mqtt-on-change |
flag | on | Publish immediately when data changes (in addition to the periodic interval). | |
--home-assistant |
flag | off | Enable Home Assistant MQTT discovery. Requires --mqtt. |
|
--ha-discovery-prefix |
string | homeassistant |
Home Assistant discovery topic prefix. | |
--ha-device-id |
string | none (derived from prefix) | Device identifier for HA discovery. Requires --home-assistant. |
Security: Without --mqtt-tls, MQTT credentials (--mqtt-username / --mqtt-password) travel in cleartext. --mqtt-tls-skip-verify disables certificate validation and is not for production — it defeats the point of TLS.
When you enable --mqtt-tls and do not also set --mqtt-port explicitly, the port switches from 1883 to 8883 automatically.
Dependencies: --mqtt-client-cert and --mqtt-client-key are mutually required; --home-assistant requires --mqtt; --ha-device-id requires --home-assistant.
Alerting options¶
Fault detection and notifications. Lives in the Alerting settings area.
| Option | Short | Type | Default | Description |
|---|---|---|---|---|
--alert-salt-low-ppm |
uint32 | 2600 |
Salt PPM threshold for the salt-low alert. 0 disables the check. Valid range 0–6000. |
|
--alert-webhook-url |
string | none | Webhook URL POSTed on every alert transition. Must be an absolute http:// or https:// URL. |
|
--alert-comms-timeout-seconds |
uint32 | 60 |
Seconds without a decoded message before the serial-comms-loss alert raises. Must be greater than 0. |
--alert-salt-low-ppm outside 0–6000, an --alert-webhook-url that is not an absolute http(s) URL, or --alert-comms-timeout-seconds of 0 each fail validation and stop startup.
History options¶
Time-series persistence for trends. Lives in the History settings area.
| Option | Short | Type | Default | Description |
|---|---|---|---|---|
--history-db |
string | none | SQLite database path. Empty (the default) disables history; the read API returns 503. |
|
--history-retention-days |
uint32 | 90 |
Days of samples to keep before a daily purge. Must be greater than 0. | |
--history-flush-seconds |
uint32 | 10 |
Interval at which buffered samples flush to disk. Must be greater than 0. |
History is off until you set --history-db. With no database path, the history read API returns 503 and nothing is recorded.
Equipment options¶
Pool topology and the discovered-equipment cache. Lives in the Equipment settings area.
| Option | Short | Type | Default | Description |
|---|---|---|---|---|
--pool-configuration |
enum | auto |
Installation type. Accepted: pool-only, spa-only, combo, dual, auto. |
|
--equipment-cache-file |
string | none | JSON cache of discovered equipment for an instant dashboard on restart. Empty disables caching. | |
--temperature-staleness-threshold |
uint | 600 |
Age in seconds after which a reported temperature is flagged stale. |
--pool-configuration maps to the internal body model:
| Value | Meaning |
|---|---|
pool-only |
Single body. |
spa-only |
Single body (same model as pool-only). |
combo |
Dual body, shared equipment. |
dual |
Dual body, separate equipment. |
auto |
Autodetect from the bus (the default; the configuration is treated as not user-specified). |
Scheduling and preferences options¶
Persistence for schedules and user preferences. These are two separate areas (Scheduling and Preferences).
| Option | Short | Type | Default | Description |
|---|---|---|---|---|
--schedules-file |
string | none | JSON file persisting schedules. Empty disables the scheduler; its CRUD routes return 503. |
|
--preferences-file |
string | none | JSON file persisting user/admin preferences. Empty keeps preferences in-memory only. |
With no --schedules-file, the scheduler is disabled and the schedule CRUD routes return 503. With no --preferences-file, preferences stay in memory and reset on restart — the preferences API still works for the session.
Matter options¶
The Matter (smart-home) bridge sidecar. Lives in the Matter settings area.
| Option | Short | Type | Default | Description |
|---|---|---|---|---|
--matter |
bool | on | Enable the Matter bridge sidecar. Opt-out: --matter false (or matter = false) turns it off. |
|
--matter-storage-path |
string | /data/matter |
Directory the sidecar persists fabric/commissioning state to. | |
--matter-status-port |
uint16 | 8099 |
Port the sidecar's status/QR server listens on. | |
--matter-passcode |
uint32 | 0 |
Commissioning passcode (8 digits). 0 lets the sidecar generate and persist one. |
|
--matter-discriminator |
uint16 | 0 |
Commissioning discriminator (0–4095). 0 lets the sidecar generate and persist one. |
Note: --matter is an opt-out flag, and it behaves unlike the other on/off flags. It takes a value:
- Absent, or bare
--matter, leaves the bridge on (the default). --matter false(command line) ormatter = false(config) turns it off.
The C++ app does not host the Matter stack itself; it launches a Node.js sidecar. For commissioning and the Docker host-networking requirement, see MATTER.md.
Jandy emulation options¶
Which Jandy/Zodiac devices the app emulates on the bus. Lives in the Jandy settings area.
| Option | Short | Type | Default | Description |
|---|---|---|---|---|
--jandy-disable-emulation |
flag | off | Disable Jandy controller emulation entirely. | |
--jandy-disable-presence-gating |
flag | off | Disable RSSA presence-gating (do not auto-suppress an emulated Serial Adapter when a real one is suspected on the bus). | |
--jandy-auto-startup |
flag | off | Auto-detect the controller from the bus and choose what to emulate (overrides --jandy-device-type). |
|
--jandy-device-type |
enum list | onetouch iaq serialadapter |
Space-separated emulation types. Accepted: rs_keypad, onetouch, iaq, pda, serialadapter, spasideremote, unknown (case-insensitive). |
|
--jandy-device-id |
hex list | per-type (see below) | Space-separated hex device IDs (0xNN), one per --jandy-device-type in order. |
|
--jandy-nav-password |
string | "" |
4-digit password for navigating password-protected Jandy menus. |
When --jandy-device-type is not given, the app emulates OneTouch, IAQ, and SerialAdapter (OneTouch for menu scraping, IAQ for status data, SerialAdapter for commands).
Default device IDs, assigned per type when you do not give an explicit --jandy-device-id:
| Type | Default ID |
|---|---|
onetouch |
0x41 |
iaq |
0xA1 |
rs_keypad |
0x09 |
pda |
0x61 |
serialadapter |
0x48 |
Both --jandy-device-type and --jandy-device-id are multi-value: list types space-separated, then list IDs in the same order. Any type without a matching ID gets its default from the table above.
Conflicts and dependencies: --jandy-device-id requires --jandy-device-type; --jandy-disable-emulation conflicts with both --jandy-device-type and --jandy-device-id.
Pentair options¶
The Pentair settings area is registered but contributes no command-line options. Pentair protocol support is auto-detected from the wire; there is nothing to configure here.
Developer and replay options¶
Diagnostics, capture recording, and deterministic replay. Lives in the Developer settings area. The full record/replay workflow is in RECORD_REPLAY.md.
| Option | Short | Type | Default | Description |
|---|---|---|---|---|
--dev-mode |
flag | off | Enable developer mode. Required to gate --replay-filename. |
|
--replay-filename |
string | none | Replay file to source test data from. | |
--record-serial |
string | none | Record serial port data to a file for later replay. The path is taken as given (relative to the working directory) and is not confined to --capture-directory. |
|
--capture-directory |
string | captures |
Directory that on-demand captures (started from the web UI's Diagnostics page or POST /api/diagnostics/recording) are written to and downloaded from. Relative paths resolve against the working directory; it is created on the first recording. |
|
--decode-to-master |
flag | off | Decode and log RS-485 frames addressed to the master (0x00); observe-only. |
|
--replay-frame-period |
uint32 | 15 |
Capture-replay inter-frame period in milliseconds. 0 = unpaced (as fast as possible). |
|
--replay-speed |
double | 1.0 |
Replay speed factor scaling --replay-frame-period (>1 faster, <1 slower). |
|
--profiler |
enum | none | Profiling tool. Accepted: tracy, uprof, vtune (case-insensitive). |
|
--loglevel-<channel> |
enum | none | Set the log level for one logging channel. One option per channel (see below). |
There is one --loglevel-<channel> option per logging channel. The 18 channels are:
certificates coroutines developer devices equipment exceptions
main messages mqtt navigation options platform
profiling protocol scraping serial signals web
Each takes a severity (case-insensitive): Trace, Debug, Info, Notify, Warning, Error, Fatal. For example --loglevel-protocol Debug.
Audit is not a log channel. The security audit trail (auth decisions, privileged actions) is a separate subsystem, not an operational log channel, so there is deliberately no
--loglevel-audit. See Security → Audit trail for where audit events are routed.
Where on-demand captures are written¶
--capture-directory is the one directory the runtime recording feature is allowed to touch. The filename supplied by POST /api/diagnostics/recording is jailed into it as a bare *.cap basename — path separators, drive letters, leading separators, .., control characters, and quotes are all rejected — and GET /api/diagnostics/recording/captures/{filename} serves back out of that same jail, so neither endpoint can reach a file elsewhere on the host.
The default (captures, relative to the working directory) keeps existing installs unchanged. Point it somewhere the operator can actually reach when the app runs somewhere they cannot get a shell — this is exactly what the Home Assistant add-on does, mapping it onto its user-browsable app_config share (see Home Assistant add-on).
--record-serial is unaffected: it is an operator-supplied path on the command line, so it is used as given.
Logging sinks¶
Where operational log records are delivered is configured by the Logging
options. By default (--log-sinks auto) the destination is derived from the run
environment: an interactive terminal or a container gets the console; under
systemd the console additionally carries sd-daemon <N> priority prefixes so
journalctl -u aqualink-automate -p warning works.
| Option | Type | Default | Notes |
|---|---|---|---|
--log-sinks |
string | auto |
auto (environment-derived) or a comma-separated list of console, native, file, journald. native adds the OS-native operational sink (syslog on POSIX, the Windows Event Log); file requires --log-file; journald is the structured native journal sink on Linux when libsystemd is present at runtime (elsewhere it falls back to console with priority prefixes). Under auto: --log-file implies the file sink, and under systemd (stderr is the journal) with libsystemd available, journald is used automatically. |
--log-syslog-facility |
enum | daemon |
POSIX syslog facility for the general native sink: daemon, user, local0–local7 (case-insensitive). Ignored on Windows. The audit sink always uses authpriv regardless. |
--log-format |
enum | text |
Record format for the console and file sinks: text (human) or json (one JSON object per line, for container / SIEM pipelines). The native/syslog sink is always message-only. |
--log-file-format |
enum | (follows --log-format) |
Record format for the file sink only: text or json. Unset, the file uses --log-format, so existing setups are unchanged. Set it to json to keep a readable text console while the file carries one timestamped (ts) JSON object per line — the text format has no wall-clock time, only a sequence number. Invalid values fail validation at startup. |
--log-file |
path | (unset) | Write logs to this file (enables the file sink). Rotated and size-bounded; on shutdown the active file is rotated into the kept set. |
--log-file-max-size |
bytes | 10485760 (10 MiB) |
Rotate the log file when it would exceed this many bytes. |
--log-file-max-files |
count | 5 |
Keep at most this many rotated log files (oldest deleted). |
Examples: --log-sinks console,native --log-syslog-facility local0 (console plus
a syslog local0 operational sink); --log-file /var/log/aqualink/app.log
--log-format json (console and a rotating JSON file, since auto adds the
file sink when a path is given); --log-file /var/log/aqualink/app.log
--log-file-format json (readable text console plus a timestamped JSON file —
what the Home Assistant add-on's log_to_file option does);
--log-sinks console --log-format json (JSON to the console only, ideal behind a
container log driver).
Platform-native sinks. On Linux, when libsystemd.so.0 is present (it is on any
systemd host), journald delivers records to the journal with structured fields
(query with e.g. journalctl AA_CHANNEL=Web), and auto selects it when running
under systemd. libsystemd is loaded at runtime (dlopen), so it is neither a build
nor a hard runtime dependency — if it is absent, logging falls back to the console. On macOS the
native sink uses the unified logging system (os_log; view with Console.app or
log show). On Windows, the Event Log source is registered once, elevated,
with --register-log-source (and removed with --unregister-log-source); the
running service never needs elevation because the sink itself does not write the
registry. --install-service performs this registration too, so a service install is
clean in one step. The executable embeds a message-table resource matching the sink's
event IDs, so once the source is registered Event Viewer renders each entry verbatim;
until that one-time registration, events still appear in the Application log, just
wrapped in a generic description.
Running as a Windows service¶
On Windows the application can run as a managed service (the analogue of the Linux systemd unit): auto-start at boot, survives logoff, restarts on failure, and logs to the Windows Event Log. Install it from an elevated prompt:
aqualink-automate.exe --install-service --config "C:\ProgramData\Aqualink Automate\aqualink-automate.conf"
--install-service registers an auto-starting (delayed) service running as the built-in
least-privilege NT AUTHORITY\LocalService account, sets restart-on-failure recovery,
and registers the Event Log source. Any flags you pass alongside --install-service
(--config, ports, --mqtt-*, …) are baked into the service command line, mirroring
the systemd unit's ExecStart — so that is where the service reads its configuration
from. If you omit --config, it defaults to
%ProgramData%\Aqualink Automate\aqualink-automate.conf, creating that folder and a
starter file. Use absolute paths: a service's working directory is System32.
Manage it with the standard tools — sc start Aqualink-Automate /
Start-Service Aqualink-Automate, Stop-Service, Get-Service, sc qc Aqualink-Automate
(shows the baked-in command line) — and remove it with an elevated
aqualink-automate.exe --uninstall-service (which also removes the Event Log source).
Run from a normal console (no --install-service) the application behaves exactly as
before — a foreground process that shuts down cleanly on Ctrl-C.
Important — the big replay gotcha: --replay-filename has hard dependencies. A bare --dev-mode --replay-filename file.cap fails validation. --replay-filename requires all of:
--dev-mode--profiler <tool>- every
--loglevel-<channel>option (all 18 must be present)
It also conflicts with --record-serial. A complete, validation-passing replay invocation is in the Worked examples.
Non-obvious behaviors and gotchas¶
- The command line wins over the config file. Merging is first-write-wins, and the command line is parsed first. A flag on the command line cannot be overridden by the config file.
- A missing
--configfile is fatal; a missing--configflag is fine. If you point--configat a path that does not exist, the app exits. - Unknown config keys are silently warned, not fatal. A typo'd key has no effect and only shows up in the log. Unknown command-line arguments, by contrast, stop startup.
- The real default is the settings-struct default, not always the boost default. Each option is only written into settings when it is present on the command line or in the config; otherwise the value initialized in the settings struct's constructor applies. For most options these match, but
--serial-portis a known case: the struct initializes/dev/ttyUSB0, yet the option carries a platform default (/dev/ttyS0/COM1) that wins because the option is always present via its default value. --matteris opt-out and value-bearing. Unlike the on/off flags, it takes a value; use--matter falseto disable.- Disable flags invert their setting.
--disable-httpand--disable-httpsturn the respective server off; both servers are on by default. - Empty-string settings disable subsystems.
--history-db,--schedules-file, and--equipment-cache-fileeach disable their feature when left empty (the default). History and scheduling routes then return503. - Defaulted options never trigger conflict or dependency checks. See the matrix below — a check fires only when the relevant option is explicitly set, not when it is sitting at its default.
Conflict and dependency matrix¶
Two validation rules run after parsing:
- Conflict: two options that cannot be set together. The check fires only when both are present and neither is defaulted — so two defaulted options, or one explicit plus one defaulted, never conflict.
- Dependency: option A requires option B. The check fires when A is present-and-not-defaulted while B is absent or sitting at its default.
| Area | Type | Options |
|---|---|---|
| App | conflict | --debug ↔ --trace |
| Serial | conflict | --serial-port ↔ --remote-serial-port |
| Serial | conflict | --rfc2217 ↔ --rawtcp |
| Serial | conflict | --rfc2217 ↔ --no-rfc2217 |
| Serial | conflict | --no-rfc2217 ↔ --rawtcp |
| Web | conflict | --disable-http ↔ --http-port |
| Web | conflict | --disable-https ↔ --https-port |
| Web | conflict | --disable-https ↔ --cert |
| Web | conflict | --disable-https ↔ --cert-key |
| Web | conflict | --disable-https ↔ --cachain-cert |
| Web | dependency | --cert requires --cert-key (and vice versa) |
| MQTT | dependency | --mqtt-client-cert requires --mqtt-client-key (and vice versa) |
| MQTT | dependency | --home-assistant requires --mqtt |
| MQTT | dependency | --ha-device-id requires --home-assistant |
| Jandy | conflict | --jandy-disable-emulation ↔ --jandy-device-type |
| Jandy | conflict | --jandy-disable-emulation ↔ --jandy-device-id |
| Jandy | dependency | --jandy-device-id requires --jandy-device-type |
| Developer | conflict | --replay-filename ↔ --record-serial |
| Developer | dependency | --replay-filename requires --dev-mode |
| Developer | dependency | --replay-filename requires --profiler |
| Developer | dependency | --replay-filename requires every --loglevel-<channel> |
Worked examples¶
Remote serial over RFC2217¶
Connect to an Aqualink RS controller through a serial-to-Ethernet bridge, emulating a OneTouch device, with the web UI on all interfaces over plain HTTP:
aqualink-automate \
--remote-serial-port 192.168.1.50:8899 \
--rfc2217 \
--jandy-device-type onetouch --jandy-device-id 0x41 \
--address 0.0.0.0 \
--disable-https
The same thing as a config file:
remote-serial-port = 192.168.1.50:8899
rfc2217 = true
jandy-device-type = onetouch
jandy-device-id = 0x41
address = 0.0.0.0
disable-https = true
MQTT with Home Assistant discovery¶
Publish to a broker and advertise entities to Home Assistant:
aqualink-automate \
--serial-port /dev/ttyUSB0 \
--mqtt --mqtt-host 192.168.1.100 --mqtt-port 1883 \
--mqtt-username pool --mqtt-password secret \
--home-assistant --ha-discovery-prefix homeassistant
Security: without --mqtt-tls, those credentials cross the network in cleartext. For the full topic and entity reference, see mqtt-home-assistant.md.
Developer replay¶
--replay-filename requires --dev-mode, --profiler, and every --loglevel-<channel>. This invocation passes validation (it mirrors the one the UI end-to-end tests boot with):
aqualink-automate \
--dev-mode \
--replay-filename test/fixtures/sample_session.cap \
--profiler tracy \
--http-port 18080 --address 127.0.0.1 --disable-https \
--doc-root assets/web \
--jandy-disable-emulation \
--loglevel-certificates info --loglevel-coroutines info \
--loglevel-developer info --loglevel-devices info \
--loglevel-equipment info --loglevel-exceptions info \
--loglevel-main info --loglevel-messages info \
--loglevel-mqtt info --loglevel-navigation info \
--loglevel-options info --loglevel-platform info \
--loglevel-profiling info --loglevel-protocol info \
--loglevel-scraping info --loglevel-serial info \
--loglevel-signals info --loglevel-web info
Dropping any one --loglevel-<channel>, --profiler, or --dev-mode makes the command fail validation at startup. See RECORD_REPLAY.md for recording fixtures and the replay workflow.