A web-based automation system for intelligent hot tub temperature management and equipment control. Control your hot tub heater, pump, and ionizer remotely via a mobile-friendly web interface with scheduling, temperature monitoring, predictive heating, and safety features.
If you have a hot tub without built-in smart controls, this project lets you:
- Turn heating on/off remotely - No more walking outside in the cold to check if the tub is ready
- Heat to a target temperature - Set a desired temperature and the system automatically manages the heater, stopping when the target is reached
- "Ready By" scheduling - Tell the system what time you want the tub ready, and it calculates when to start heating based on cooling and heating models
- Schedule heating in advance - Create one-time or recurring daily schedules
- Monitor water temperature - Check current water and ambient temperature from anywhere
- Automate heating cycles - Schedule heater on, then auto-off after a set duration
- Run the circulation pump - Keep water clean with scheduled ionizer/pump cycles
- Skip upcoming schedules - Temporarily skip the next occurrence of a recurring job without deleting it
The system uses IFTTT webhooks to control SmartLife/Tuya smart relays, making it compatible with most affordable smart home equipment.
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Web Browser │───▶│ PHP Backend │───▶│ IFTTT Webhooks │
│ (SvelteKit) │◀───│ REST API │ └────────┬────────┘
└─────────────────┘ └────────┬────────┘ │
│ ▼
│ ┌─────────────────┐
│ │ SmartLife/Tuya │
┌────────▼────────┐ │ Smart Relays │
│ ESP32 Sensor │ └────────┬────────┘
│ (DS18B20) │ │
└─────────────────┘ ▼
┌─────────────────┐
│ Hot Tub │
│ Equipment │
└─────────────────┘
- Mobile-First Web UI - Dark-themed, responsive interface optimized for phones
- One-Tap Controls - Quick buttons for heater on/off and pump activation with active glow state
- Equipment Status Display - Control buttons illuminate when equipment is active
- Cancel Feedback - Buttons show "Cancelling..." state with visual dimming during API calls
- Automatic Temperature Control - Set a target temperature (80-110°F) and the system manages the heater automatically
- Stall Detection - Detects when water temperature stops rising (e.g., heater malfunction) and shuts down safely
- Configurable Stall Parameters - Adjust grace period and timeout for stall detection
- Quick Scheduling - Pre-configured buttons to heat "In 30 min", "In 1 hour", etc.
- Full Scheduler - Create one-time or recurring daily heating schedules
- "Ready By" Mode - Schedule by desired ready time; the system uses cooling and heating models to calculate the optimal start time
- Auto Heat-Off - Automatically schedule heater shutdown after configurable duration
- Skip/Unskip - Temporarily skip the next occurrence of a recurring job and resume later
- Auto-Refresh - Schedule panel automatically refreshes when a job's scheduled time passes
- Heating Characteristics - Analyzes past heating sessions to determine heating velocity (°F/min), startup lag, and overshoot
- Newton's Law Cooling Model - Fits a cooling curve to historical data to predict temperature decay over time
- Precision Start Calculation - "Ready By" mode uses heating and cooling models to project the optimal time to begin heating
- Real-Time Display - Water and ambient temperature from ESP32 sensors
- Multi-Sensor Support - Configure multiple DS18B20 sensors with assigned roles (water, ambient)
- Sensor Calibration - Per-sensor calibration offsets for accuracy
- Dynamic Reporting Intervals - ESP32 reports every 5 minutes normally, switching to 1-minute intervals when the heater is active for tighter temperature control
- Temperature History - Daily temperature logs for analysis
- OTA Firmware Updates - ESP32 firmware updated over-the-air via HTTP; the device checks for updates each time it reports temperature
- JWT Authentication - Secure login with httpOnly cookie support
- Three User Roles - Admin (full access), User (controls + scheduling), Basic (controls + temperature only)
- User Management - Admin interface for creating, managing, and deleting users
- Password Management - Admin can reset user passwords
- Healthchecks.io Integration - Optional monitoring for cron job alerts
- Request Logging - All API requests logged in JSON Lines format with automatic rotation
- Equipment Event Log - Timestamped record of all heater/pump actions and stall events
- Crontab Backups - Automatic timestamped backups before crontab modifications
- Orphan Job Cleanup - Maintenance endpoint to clean up stale job files
ESP32 with DS18B20
- ESP32 Development Board - WiFi-enabled microcontroller
- ESP32-WROOM-32 based board (NodeMCU, DevKit, etc.) such as this one.
- DS18B20 Temperature Sensor - Waterproof digital thermometer
- 1-Wire interface, accurate to 0.5C
- 4.7K pull-up resistor required (connect between data and VCC) such as this one
- PlatformIO - For firmware development and flashing
- See
esp32/directory for firmware code
- See
- Smart Relay Controller - IFTTT-compatible device for pump and heater control
- Compatible: SmartLife or Tuya-based smart switches/relays such as this one
- Example: 4-channel WiFi relay modules with smartphone app
- IFTTT Account - Free tier works fine for webhook integrations
- Sign up: IFTTT.com
The system requires IFTTT webhook events connected to SmartLife/Tuya "scenes". Since IFTTT cannot directly control individual SmartLife switches, you create scenes that IFTTT can trigger.
If you use other smart home control systems, they will work fine as long as IFTTT can trigger the appropriate events associated with the Webhooks described below.
| Webhook Event | Purpose | SmartLife Scene |
|---|---|---|
hot-tub-heat-on |
Start heating | Turn on pump, wait 60s, turn on heater |
hot-tub-heat-off |
Stop heating | Turn off heater, wait 90s, turn off pump |
cycle_hot_tub_ionizer |
Run pump/ionizer | Turn on pump for 2 hours |
Heat On Scene (hot-tub-heat-on):
1. Turn ON hot tub water pump
2. Wait 60 seconds (allows water circulation)
3. Turn ON hot tub heater
Heat Off Scene (hot-tub-heat-off):
1. Turn OFF hot tub heater
2. Wait 90 seconds (cooling circulation)
3. Turn OFF hot tub water pump
This sequencing protects heating elements by ensuring proper water flow during operation.
- Go to IFTTT Create
- If This: Choose "Webhooks" → "Receive a web request"
- Event Name: Enter exactly
hot-tub-heat-on(case-sensitive) - Then That: Choose your controller system such as "SmartLife" → "Activate scene"
- Scene: Select your "Heat On" scene or however your backend controller works
- Save and repeat for other webhooks
Get your webhook key from: https://ifttt.com/maker_webhooks/settings
cd backend
composer install
# Copy development config (uses stub mode - no real hardware triggers)
cp config/env.development .env
# For production, use:
# cp config/env.production.example .env
# Then edit .env to add your real API keys
# Start development server
php -S localhost:8080 -t publiccd frontend
npm install
npm run dev # Starts on http://localhost:5173Default login: admin / password (change in production!)
For temperature sensing:
cd esp32
# Create .env file with your WiFi and API credentials
cat > .env << 'EOF'
WIFI_SSID=your-wifi-network
WIFI_PASSWORD=your-wifi-password
ESP32_API_KEY=your-secure-api-key
API_ENDPOINT=http://your-server.com/api/esp32/temperature
EOF
# Install PlatformIO (if not already installed)
# pip install platformio
# Build and upload firmware
pio run --target upload
# Monitor serial output
pio device monitorThe ESP32 will:
- Connect to WiFi and read temperature from DS18B20 sensors (GPIO 4)
- Sync time via NTP for coordinated reporting, aligned to the :53 second mark
- POST temperature readings every 5 minutes normally, or every 1 minute when the heater is active (interval is server-controlled)
- Check for OTA firmware updates on each report and install automatically
- Use exponential backoff on failures (10s to 5 min)
- Auto-reboot after 30 minutes of continuous failures
Hardware wiring:
- DS18B20 VCC to ESP32 3.3V
- DS18B20 GND to ESP32 GND
- DS18B20 DATA to ESP32 GPIO 4
- 4.7K resistor between DATA and VCC
The ESP32 includes a telnet debugger for remote diagnostics without physical access:
# Connect to ESP32 (default port 23)
telnet <esp32-ip-address>
# Or using netcat
nc <esp32-ip-address> 23Available commands:
| Command | Description |
|---|---|
help |
Show available commands |
diag |
Full diagnostics (WiFi, sensors, readings) |
scan |
Scan OneWire bus for sensors |
read |
Read all sensor temperatures |
info |
Show firmware version, IP, uptime |
ota |
Show OTA update status |
Example diagnostic output:
--- Connection Info ---
Firmware: 1.3.0
WiFi SSID: your-network
IP Address: 192.168.1.100
Signal Strength: -74 dBm
Uptime: 3600 seconds
--- Sensor Readings ---
Sensor 0 (28:B4:51:02:00:00:00:9A):
Temperature: 39.38 C / 102.88 F
Status: OK
The ESP32 supports over-the-air firmware updates via HTTP. Updates are pulled automatically when the device reports temperature to the API.
How it works:
- ESP32 reports its current firmware version when posting temperature data
- Server responds with new firmware info if an update is available
- ESP32 downloads and installs the update automatically
- Device reboots into new firmware
Deploying a new firmware version:
cd esp32
# 1. Update version number in src/main.cpp
# #define FIRMWARE_VERSION "1.4.0"
# 2. Build the firmware
pio run
# 3. Copy binary to backend storage
cp .pio/build/esp32dev/firmware.bin \
../backend/storage/firmware/firmware-1.4.0.bin
# 4. Update firmware config
cat > ../backend/storage/firmware/config.json << 'EOF'
{
"version": "1.4.0",
"filename": "firmware-1.4.0.bin",
"updated_at": "2026-01-22T12:00:00-08:00",
"notes": "Description of changes"
}
EOF
# 5. Commit and deploy via PR to production
git add -A
git commit -m "Deploy ESP32 firmware v1.4.0"
git push origin main
# Create PR from main to production and mergeManual USB flash (if OTA unavailable):
cd esp32
pio run -t upload
pio device monitor # View boot logsFirmware storage structure:
backend/storage/firmware/
├── config.json # Current version metadata
├── firmware-1.3.0.bin # Firmware binary (tracked in git)
└── firmware-1.2.0.bin # Previous versions (optional)
All configuration is via backend/.env:
# Application mode
APP_ENV=development
# External API Mode - controls ALL external services (IFTTT, Healthchecks.io)
EXTERNAL_API_MODE=stub # stub (safe) or live (requires keys)
# IFTTT webhook integration
IFTTT_WEBHOOK_KEY=your-key # From IFTTT Maker Webhooks settings
# ESP32 temperature sensor
ESP32_API_KEY=your-secure-api-key # Secure API key for ESP32 authentication
# Healthchecks.io monitoring (optional)
HEALTHCHECKS_IO_KEY=your-key # API key for cron job monitoring
HEALTHCHECKS_IO_CHANNEL=your-channel # Notification channel slug
# Authentication
AUTH_ADMIN_USERNAME=admin
AUTH_ADMIN_PASSWORD=change-this!
JWT_SECRET=generate-a-secure-random-string
# API Base URL - Required for scheduled jobs (used by cron)
API_BASE_URL=https://your-server.com/path/to/backend/public| Mode | Description |
|---|---|
stub |
Simulates all API calls - safe for development/testing |
live |
Makes real API calls to IFTTT and Healthchecks.io |
The unified EXTERNAL_API_MODE defaults to stub for fail-safe behavior.
| Role | Capabilities |
|---|---|
admin |
Full access: equipment control, scheduling, settings, user management, heating analysis |
user |
Standard access: equipment control, scheduling, settings |
basic |
Simplified UI: equipment control and temperature display only |
The basic role is useful for household members who just need to turn the hot tub on/off without the complexity of scheduling features.
# Run ALL tests (backend + frontend + ESP32 + E2E)
./scripts/test.sh
# Individual suites
./scripts/test.sh backend # PHP backend tests
./scripts/test.sh frontend # SvelteKit unit tests
./scripts/test.sh esp32 # ESP32 native tests
./scripts/test.sh e2e # Playwright E2E testshot-tub-controller/
├── backend/ # PHP REST API
│ ├── public/ # Web root (index.php entry point)
│ ├── src/
│ │ ├── Controllers/ # API endpoint handlers
│ │ ├── Services/ # Business logic, external API clients
│ │ ├── Middleware/ # Auth, CORS
│ │ └── Routing/ # URL router
│ ├── storage/ # Job files, logs, user data, firmware
│ └── tests/ # PHPUnit tests
│
├── frontend/ # SvelteKit web UI
│ ├── src/
│ │ ├── lib/components/ # UI components
│ │ ├── lib/stores/ # State management
│ │ └── routes/ # Pages
│ └── e2e/ # Playwright E2E tests
│
├── esp32/ # ESP32 firmware (PlatformIO)
│ ├── src/ # Main firmware code
│ ├── lib/ # ApiClient library
│ └── test/ # Unity unit tests
│
└── CLAUDE.md # Development guidelines
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/health |
Health check with equipment status and heat-target settings |
| POST | /api/auth/login |
Login (sets httpOnly cookie) |
| POST | /api/auth/logout |
Logout (clears cookie) |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/auth/me |
Current user info |
| POST | /api/equipment/heater/on |
Turn heater on via IFTTT |
| POST | /api/equipment/heater/off |
Turn heater off via IFTTT |
| POST | /api/equipment/pump/run |
Run pump cycle via IFTTT |
| POST | /api/equipment/heat-to-target |
Start heat-to-target session |
| GET | /api/equipment/heat-to-target |
Get heat-to-target status |
| DELETE | /api/equipment/heat-to-target |
Cancel heat-to-target session |
| GET | /api/temperature |
Get current water/ambient temperatures |
| GET | /api/settings/heat-target |
Get heat-to-target settings |
| POST | /api/schedule |
Schedule a job (one-time or recurring) |
| GET | /api/schedule |
List scheduled jobs |
| DELETE | /api/schedule/{id} |
Cancel a scheduled job |
| POST | /api/schedule/{id}/skip |
Skip next occurrence of recurring job |
| DELETE | /api/schedule/{id}/skip |
Unskip a recurring job |
| GET | /api/esp32/sensors |
List ESP32 sensors with config |
| PUT | /api/esp32/sensors/{address} |
Update sensor role/calibration |
| Method | Endpoint | Description |
|---|---|---|
| PUT | /api/settings/heat-target |
Update heat-to-target settings |
| GET | /api/admin/heating-characteristics |
Get heating/cooling analysis |
| POST | /api/admin/heating-characteristics/generate |
Generate heating analysis |
| GET | /api/admin/crontab |
View current crontab entries |
| GET | /api/users |
List users |
| POST | /api/users |
Create user |
| DELETE | /api/users/{username} |
Delete user |
| PUT | /api/users/{username}/password |
Reset user password |
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/esp32/temperature |
Receive temperature data from ESP32 (API key auth) |
| POST | /api/maintenance/logs/rotate |
Rotate log files |
| POST | /api/maintenance/jobs/cleanup |
Clean up orphaned job files |
| POST | /api/maintenance/heat-target-check |
Heat-to-target temperature check |
| POST | /api/maintenance/dtdt-wakeup |
DTDT ready-by wakeup handler |
This system controls real electrical equipment. Safety features include:
- Test Mode: Always develop with
EXTERNAL_API_MODE=stub - Equipment Sequencing: Pump starts before heater, heater stops before pump
- Stall Detection: Heat-to-target automatically shuts down if temperature stops rising
- Authentication Required: All control endpoints require login
- Audit Logging: All API requests and equipment events are logged with timestamps
- File Locking: Concurrent heat-to-target operations are prevented via
flock
Never deploy to production without:
- Changing default passwords
- Setting a secure JWT_SECRET
- Configuring HTTPS
- Testing equipment sequencing with your actual setup
The system is designed to run on "low cost web hosting services" such as cPanel hosts. The main requirement is that your host must allow cron job scheduling (and scheduled crons must execute reliably). Otherwise it uses only basic HTTP APIs between front and backend. The ESP32 thermometer integration is "receive only" - so the ESP32 device sends temperature on a schedule, which the backend can alter whenever the device phones in (to increase or decrease the frequency of reporting).
Due to the low cost host, "real" deployment techniques may not be available. Currently I'm deploying this to my host via FTP+SSL in a Github Action, which is working perfectly thanks to this free tool:
All API requests are logged in JSON Lines format to backend/storage/logs/api-requests-*.jsonl:
- Timestamp, IP address, HTTP method, URI
- Response status and duration
- Authenticated username (if applicable)
All heater/pump actions are recorded to backend/storage/logs/equipment-events.log:
- Heater on/off events with water temperature at time of action
- Stall detection events
- Used by the heating characteristics analysis
Automated log rotation runs monthly via cron:
- Compress logs older than 30 days
- Delete compressed logs older than 6 months
- Crontab backups follow the same retention policy
When HEALTHCHECKS_IO_KEY is configured, scheduled jobs are monitored:
- Health check created when job is scheduled
- Alert triggered if job fails to execute within timeout
- Check deleted on successful completion
This provides proactive notification if a scheduled heating job fails to fire.
This project uses Test-Driven Development (TDD):
- RED: Write a failing test first
- GREEN: Write minimal code to pass
- REFACTOR: Clean up while tests stay green
See CLAUDE.md for detailed development guidelines.
Licensed under the Apache License, Version 2.0.
Stephen Midgley