π€ 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.binfiles, 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
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:
- Installs Docker if missing
- Downloads deployment files (
docker-compose.yml,nginx/,.env) - Configures the stack interactively (Wi-Fi, secrets, environment)
- Downloads the agent binary
- Installs and starts the
station-agent.servicesystemd 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:
- NetworkManager spins up a Wi-Fi access point
- Nginx is stopped to free port 80
- Agent serves a setup page at port 80 with a Wi-Fi network picker
- Captive portal detection (iOS/Android/Windows/macOS) auto-opens the page
- User submits SSID + password β agent connects, tears down hotspot, restarts nginx
- 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β
| Variable | Description |
|---|---|
STATION_ID | Station UUID |
UPDATE_SERVER_URL | URL to release.json |
Docker Updatesβ
| Variable | Default | Description |
|---|---|---|
COMPOSE_PROJECT_PATH | ~/smart-home | Path to compose project |
COMPOSE_FILE | docker-compose.yml | Compose file name |
CHECK_INTERVAL_MINUTES | 60 | Update poll interval |
AUTO_UPDATE | true | Auto-pull on new version |
BOOTSTRAP_ON_START | false | Start stack on agent boot if down |
HEALTHCHECK_URL | http://localhost/api/health | Post-update healthcheck |
STABILIZATION_SECONDS | 45 | Wait before healthcheck |
DOCKER_USERNAME / DOCKER_TOKEN | β | Docker Hub creds (private images) |
DOCKER_REGISTRY | β | Custom registry |
BACKEND_CONTAINER_NAME | smart-home-backend | β |
FRONTEND_CONTAINER_NAME | smart-home-frontend | β |
Firmware OTAβ
| Variable | Default | Description |
|---|---|---|
FIRMWARE_MANIFEST_URL | {UPDATE_SERVER_URL}/../firmware/manifest.json | Manifest URL |
FIRMWARE_CACHE_DIR | ~/firmware-cache | Where .bin files land |
FIRMWARE_FOLDER | /firmware | URL path served by nginx |
FIRMWARE_CHECK_INTERVAL_MINUTES | 60 | Manifest poll interval |
BACKEND_URL | derived from HEALTHCHECK_URL | API target |
BACKEND_AGENT_TOKEN | β | Bearer for /api/ota/* |
Miscβ
| Variable | Default | Description |
|---|---|---|
PORT | 3001 | Agent HTTP port |
HOST | 127.0.0.1 | Bind address |
AGENT_TOKEN | β | Bearer for protected POST endpoints |
DATA_DIR | ~/station-agent-data | Agent 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.