Skip to main content

πŸ€– station-agent

Native Node.js binary running on the Raspberry Pi as a systemd service. Manages the Docker stack, ESP32 firmware OTA, and Wi-Fi networking (including a captive portal for first-boot setup).

Source β†— β€” built as a SEA (Single Executable Application), no Node.js needed on the host.

Responsibilities​

  • 🐳 Docker updates β€” poll release.json, pull new images, healthcheck, auto-rollback on failure
  • πŸ“‘ Firmware OTA β€” poll firmware/manifest.json, download .bin files, hand them off to the backend β†’ MQTT β†’ ESP32
  • πŸ“Ά Wi-Fi watchdog β€” state machine for monitoring, retry, fallback to hotspot
  • 🌐 Captive portal β€” first-boot or Wi-Fi loss β†’ spin up an AP with web setup form
Agent binary itself is not auto-updated

The binary is installed by install-agent.sh and managed by systemd. Updating it requires a new agent-v* tag β†’ CI rebuilds β†’ reinstall via the install script.

Installation​

One command on a fresh Raspberry Pi:

curl -fsSL https://raw.githubusercontent.com/alphaoflogic-ua/smart-home-updates/main/install-agent.sh | sudo bash

The script:

  1. Installs Docker if missing
  2. Downloads deployment files (docker-compose.yml, nginx/, .env)
  3. Configures the stack interactively (Wi-Fi, secrets, environment)
  4. Downloads the agent binary
  5. Installs and starts the station-agent.service systemd unit

Reset & Reinstall​

curl -fsSL https://raw.githubusercontent.com/alphaoflogic-ua/smart-home-updates/main/reset-station.sh | sudo bash

Removes the systemd service, agent files, the entire Docker stack (with volumes), and the Docker images. Use with care β€” wipes all device data.

Docker Update Flow​

release.json Format​

{
"version": "1.2.3",
"images": {
"backend": "andriicode/smart-home-backend:1.2.3",
"frontend": "andriicode/smart-home-frontend:1.2.3"
}
}

CI updates this file in smart-home-updates repo on every release tag.

Firmware OTA Flow​

Wi-Fi Watchdog​

State machine that keeps the Pi connected, falling back to a hotspot for re-configuration:

Captive Portal​

When entering HOTSPOT_ACTIVE:

  1. NetworkManager spins up a Wi-Fi access point
  2. Nginx is stopped to free port 80
  3. Agent serves a setup page at port 80 with a Wi-Fi network picker
  4. Captive portal detection (iOS/Android/Windows/macOS) auto-opens the page
  5. User submits SSID + password β€” agent connects, tears down hotspot, restarts nginx
  6. Wi-Fi credentials are forwarded to backend (/api/settings/agent) for ESP32 provisioning use

HTTP API​

Default port: 3001. Mutating endpoints require Authorization: Bearer <AGENT_TOKEN> if AGENT_TOKEN is configured.

# Health
curl http://localhost:3001/health

# Full status (incl. firmware cache)
curl http://localhost:3001/status

# Current/last installed app version
curl http://localhost:3001/version

# Wi-Fi status (mode, SSID, IP, hotspot, watchdog state)
curl http://localhost:3001/wifi/status

# Force update to latest
curl -X POST http://localhost:3001/update \
-H 'Authorization: Bearer <token>'

# Force update to specific version
curl -X POST http://localhost:3001/update \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{"version": "1.2.3"}'

# Manual rollback
curl -X POST http://localhost:3001/rollback \
-H 'Authorization: Bearer <token>'

Environment Variables​

Required​

VariableDescription
STATION_IDStation UUID
UPDATE_SERVER_URLURL to release.json

Docker Updates​

VariableDefaultDescription
COMPOSE_PROJECT_PATH~/smart-homePath to compose project
COMPOSE_FILEdocker-compose.ymlCompose file name
CHECK_INTERVAL_MINUTES60Update poll interval
AUTO_UPDATEtrueAuto-pull on new version
BOOTSTRAP_ON_STARTfalseStart stack on agent boot if down
HEALTHCHECK_URLhttp://localhost/api/healthPost-update healthcheck
STABILIZATION_SECONDS45Wait before healthcheck
DOCKER_USERNAME / DOCKER_TOKENβ€”Docker Hub creds (private images)
DOCKER_REGISTRYβ€”Custom registry
BACKEND_CONTAINER_NAMEsmart-home-backendβ€”
FRONTEND_CONTAINER_NAMEsmart-home-frontendβ€”

Firmware OTA​

VariableDefaultDescription
FIRMWARE_MANIFEST_URL{UPDATE_SERVER_URL}/../firmware/manifest.jsonManifest URL
FIRMWARE_CACHE_DIR~/firmware-cacheWhere .bin files land
FIRMWARE_FOLDER/firmwareURL path served by nginx
FIRMWARE_CHECK_INTERVAL_MINUTES60Manifest poll interval
BACKEND_URLderived from HEALTHCHECK_URLAPI target
BACKEND_AGENT_TOKENβ€”Bearer for /api/ota/*

Misc​

VariableDefaultDescription
PORT3001Agent HTTP port
HOST127.0.0.1Bind address
AGENT_TOKENβ€”Bearer for protected POST endpoints
DATA_DIR~/station-agent-dataAgent state directory

Filesystem Layout​

/opt/station-agent/
station-agent ← SEA binary
.env ← agent config

/var/lib/station-agent/
current_version.txt
rollback.json
compose.rollback.override.yml
compose.update.override.yml

~/firmware-cache/
esp32-climate@0.1.3.bin
esp32-climate.json ← cached metadata { version, file, checksum }
esp32-pir@0.1.3.bin
...

~/smart-home/
docker-compose.yml
.env ← stack config
nginx/

Logs​

sudo journalctl -u station-agent -f

Structured JSON logs:

{"time":"...","event":"update_start","currentVersion":"1.0.0","images":"..."}
{"time":"...","event":"healthcheck_passed","status":200}
{"time":"...","event":"update_completed","currentVersion":"1.2.3"}

Docker events: update_start, update_completed, update_available, docker_pull, docker_restart, healthcheck_passed, healthcheck_failed, rollback_started, rollback_completed, bootstrap_start, bootstrap_done.

Firmware events: firmware_download_start, firmware_download_done, firmware_update_available, firmware_ready, firmware_old_removed, firmware_check_retry, firmware_check_failed.

Reference​