Over-The-Air Firmware Update Service for ESPHome Devices
Last updated: December 20, 2025
A robust Rust-based service that automatically manages firmware updates for devices running ESPHome firmware. The service monitors for new firmware versions, coordinates updates via MQTT, and uploads firmware using the ESPHome native OTA protocol v2.
Platform Note: This application is built and tested for Linux platforms. It may also work on other operating systems, but Linux is the primary target environment.
Device Note: All testing has been done with ESP32 devices. Please advise if the other kind of devices supported by ESPHome OTA capability are not working properly.
✅ Web-Based Monitoring - Real-time device dashboard with sortable tables and configuration management
✅ ESPHome OTA Protocol v2 - Native protocol with comprehensive error handling
✅ MQTT Coordination - Device registration and update notifications
✅ Automatic Version Management - Detects and deploys latest firmware versions
✅ Device State Tracking - SQLite database tracks device status and update progress
✅ Authentication Support - MD5 and SHA256 password authentication
✅ Pushover Notifications - Real-time alerts for updates, failures, and new devices
✅ Home Assistant Integration - Uses the MQTT automatic discovery feature to have it's basic state on Home Assistant
✅ Timeout Protection - All network operations have configurable timeouts
✅ Comprehensive Error Handling - 13 distinct error codes with descriptive messages
✅ Concurrent Updates - Configurable parallel firmware uploads
✅ Deep Sleep Support - Compatible with battery-powered devices
- Rust 1.70+ (2024 edition)
- MQTT broker (e.g., Mosquitto, Home Assistant)
- Devices running ESPHome firmware with OTA enabled
- Optional: Pushover account for push notifications
# Clone the repository
git clone <repository-url>
cd ota-service
# Build the service
cargo build --release
# Copy and configure
cp config.example.yaml /etc/ota-service/config.yaml
# Edit /etc/ota-service/config.yaml with your settingsCreate a config.yaml file:
mqtt:
host: "mqtt.local"
port: 1883
client_id: "ota-service"
username: "ota-user"
password: "ota-password"
keep_alive: 60
database:
path: "/var/lib/ota-service/devices.db"
pool_size: 5
service:
name: "ota-service"
log_level: "info"
log_file_path: "/var/log/ota-service/ota-service.log"
max_concurrent_updates: 10
check_interval: 300 # 5 minutes
ota_password: "your_hex_password" # Optional OTA authentication
default_ota_port: 3232 # Default OTA port (devices can override)
firmware:
storage_path: "/var/lib/ota-service/firmware"
erase_firmware_after_upload: false # Delete firmware after successful upload
pushover: # Optional push notifications
enabled: false
api_token: "your_pushover_api_token"
user_key: "your_pushover_user_key"
device: "iphone" # Optional: specific device name
priority: 0 # -2 to 2
home_assistant: # Optional Home Assistant MQTT discovery
enabled: false
discovery_prefix: "homeassistant"
node_id: "ota_service"
device_name: "OTA Service"
manufacturer: "ESPHome OTA Service"
model: "Firmware Update Manager"
update_interval: 60 # seconds
esphome_projects: # Optional ESPHome firmware rebuild capability
enabled: false
projects_folder: "/path/to/esphome/projects" # Optional: base folder for projects
default_main_filename: "main.yaml" # Default YAML filename
esphome_venv_folder: "/path/to/.esphome/venv" # Optional: ESPHome virtual environment
web:
port: 8080
username: "admin"
password: "admin"
refresh_period: 5
edit_session_timeout: 15Configuration Notes:
erase_firmware_after_upload: When set totrue, the uploaded firmware file and all older versions for the same device are automatically deleted after successful upload. This helps manage disk space when deploying firmware to multiple devices. Set tofalse(default) to keep firmware files for reuse or rollback purposes.esphome_projects.enable: When set totrue, enables the ReBuild button in the web interface for devices with a configured project folder. This allows rebuilding firmware directly through the ESPHome CLI without manual command-line operations. Requires ESPHome to be installed and accessible.
# Run directly
./target/release/ota-service /etc/ota-service/config.yaml
# Or install as systemd service
sudo cp ota-service.service /etc/systemd/system/
sudo systemctl enable ota-service
sudo systemctl start ota-serviceThe OTA Service includes a comprehensive web-based monitoring and management interface for:
- Real-time device monitoring - WebSocket-based live updates of all registered devices
- Device management - Add, edit, and remove devices with modal dialogs
- Device details - Click on device name or ID to view complete device information in a modal window
- ESPHome firmware rebuild - Rebuild device firmware directly from the web interface (when enabled)
- Firmware update history - Track all upload attempts with success/failure status
- Configuration management - View and edit service configuration through the web UI
- Service restart - Restart the service directly from the interface
-
Configure the web server in
config.yaml:web: port: 8080 username: "admin" password: "change_this_password" # Change in production! refresh_period: 5 edit_session_timeout: 15
-
Open your browser and navigate to:
http://localhost:8080 -
Log in with your configured credentials
Devices Tab:
- Live device table with auto-refresh
- Sortable and resizable columns including device name
- Color-coded status indicators (Idle, OTA in Progress, Version Available)
- Clickable device names and IDs to view detailed information in modal window
- Device management buttons:
- Add Device - Manually add new devices with optional ESPHome project configuration
- Edit - Modify device settings (ID, name, project folder, firmware version)
- ReBuild - Compile and deploy firmware using ESPHome (when esphome_projects enabled)
- Remove - Delete devices from the database
- Device details modal showing all device information (IP, MAC, topics, statistics, etc.)
- WiFi signal strength (RSSI) with visual indicators
- Firmware version with semantic sorting
Update Log Tab:
- Complete history of firmware uploads
- Success/failure status with detailed error messages
- Sortable by device, version, status, or timestamp
Config Tab:
- View all configuration settings
- Edit configuration values with password protection
- Sensitive data automatically masked (passwords, API tokens, IP addresses)
- Changes saved to config file and require restart
See doc/WEB_INTERFACE.md for complete documentation.
Devices register themselves via MQTT by publishing to the registration topic:
{
"device_id": "device-001",
"ip_address": "192.168.1.100",
"firmware_version": "1.0.0",
"ota_readiness_topic": "devices/device-001/ready",
"ota_mode_topic": "devices/device-001/ota-mode",
"uses_deep_sleep": false,
"ota_port": 3232 // Optional: custom OTA port (uses default if omitted)
}Note on OTA Ports:
- The
ota_portfield is optional in device registration - If omitted, devices will use the
default_ota_portfrom configuration (typically 3232 for ESPHome) - Custom ports are useful for devices with non-standard OTA configurations or multiple devices behind NAT
- The service automatically uses the device-specific port when uploading firmware
1. Service periodically scans firmware directory
└─> Finds firmware files matching pattern: "device_id - version.bin"
2. Version comparison
└─> Compares available firmware with device's current version
└─> Selects highest version number
3. Availability notification
└─> Service publishes "NEW-FIRMWARE-VERSION" to device's ota_mode_topic
4. Device readiness
└─> Device responds "OTA-READY" to ota_readiness_topic
5. OTA upload (Port 3232)
└─> Service connects to device via ESPHome OTA protocol v2
└─> Authenticates (if password configured)
└─> Uploads firmware in 1024-byte chunks
└─> Receives acknowledgment every 8192 bytes
└─> Device validates MD5 checksum
6. Device restart
└─> Device installs firmware and restarts
└─> Re-registers with new version
7. Notification (if Pushover enabled)
└─> Success or failure notification sent
Place firmware files in the configured storage directory using this naming convention:
<device_id> - <version>.bin
Examples:
device-kitchen - 1.2.3.bin
device-bedroom - 2.0.1.bin
device-garage - 1.5.0.bin
The service automatically selects the highest version number for each device.
The deploy-device-firmware.sh script simplifies firmware deployment by automating the process of compiling ESPHome firmware and copying it to the OTA service firmware directory.
- ESPHome CLI installed (
pip install esphome) - ESPHome device configuration YAML files
- OTA service configured and running
- User must be member of
ota-servicegroup (for daemon installations)
If the OTA service is installed as a daemon, the firmware folder has restricted permissions (770). Users deploying firmware must be added to the ota-service group:
# Add your user to the ota-service group
sudo usermod -a -G ota-service $USER
# Log out and back in for group change to take effect
# Verify group membership
groups# Navigate to your ESPHome device configuration folder
cd /path/to/esphome-configs
# Run the deployment script with the device YAML file
/path/to/ota-service/deploy-firmware.sh device-kitchen.yaml- Validates the ESPHome YAML file exists in current directory
- Extracts device name and version from the YAML configuration
- Compiles the firmware using ESPHome CLI
- Reads OTA service configuration to find firmware storage directory
- Copies the compiled firmware with proper naming:
device_id - version.bin - Verifies the deployment was successful
Before first use, edit the OTA_CONFIG_FILE path in the script:
# TODO: Adjust this path to match your OTA service configuration file location
OTA_CONFIG_FILE="/etc/ota-service/config.yaml"INFO: Using OTA service config: /etc/ota-service/config.yaml
INFO: Firmware storage path: /var/lib/ota-service/firmware
INFO: Device name: device-kitchen
INFO: Firmware version: 1.2.3
INFO: Starting ESPHome firmware compilation...
SUCCESS: Firmware compiled successfully
INFO: Source file: /path/to/.esphome/build/device-kitchen/.pioenvs/device-kitchen/firmware.bin
INFO: Destination: /var/lib/ota-service/firmware/device-kitchen - 1.2.3.bin
SUCCESS: Firmware deployed successfully!
- The script must be run from the directory containing your ESPHome YAML file
- Device name is extracted from the
name:substitution field in the YAML - Device id is extracted from the 'device_id:' substitution field in the YAML
- Version is extracted from the
firmware_version:substitution field in the YAML - The script will not overwrite existing firmware files with the same version
Your devices must be running ESPHome with OTA enabled and proper MQTT integration. The MQTT configuration is required for the OTA service to communicate with devices.
The OTA service uses MQTT for coordination with devices:
- Registration Topic (
ota-service/registration): Devices publish to this topic to register with the service - OTA Mode Topic (
devices/<device_id>/ota-mode): Service publishesNEW-FIRMWARE-VERSIONto notify device - OTA Readiness Topic (
devices/<device_id>/ready): Device publishesOTA-READYwhen ready for firmware upload
# ESPHome configuration with OTA service integration
substitutions:
device_id: device-kitchen
firmware_version: "1.0.0" # Update this when deploying new firmware
ota_password: "d96112143a8c04d8b2945b226a9b95e7" # Must match OTA service config
esphome:
name: ${device_id}
platform: ESP32
board: esp32dev
# Register device on boot
on_boot:
priority: -100 # Run after WiFi and MQTT are connected
then:
- delay: 5s # Wait for MQTT connection to stabilize
- script.execute: register_device
wifi:
ssid: "your-ssid"
password: "your-password"
# Enable OTA updates via ESPHome protocol v2
# This allows the OTA service to upload firmware over port 3232
ota:
password: ${ota_password} # Must match ota_password in OTA service config.yaml
port: 3232 # Default ESPHome OTA port
# MQTT configuration - REQUIRED for OTA service integration
mqtt:
broker: mqtt.local
port: 1883
username: "mqtt_user" # Optional
password: "mqtt_password" # Optional
client_id: ${device_id}
# Subscribe to OTA mode topic
# Service publishes NEW-FIRMWARE-VERSION here to notify device
on_message:
- topic: devices/${device_id}/ota-mode
payload: 'NEW-FIRMWARE-VERSION'
then:
- logger.log: "New firmware available, preparing for OTA update"
# Respond immediately that device is ready
- mqtt.publish:
topic: devices/${device_id}/ready
payload: "OTA-READY"
retain: false
qos: 2
# Text sensor for IP address (needed for registration)
text_sensor:
- platform: wifi_info
ip_address:
id: device_ip
# Script to register device with OTA service
script:
- id: register_device
then:
- wait_until:
mqtt.connected:
- delay: 2s
- lambda: |-
// Build registration JSON with all required fields
std::string ip = id(device_ip).state.c_str();
std::string mac = wifi::global_wifi_component->get_mac_address_pretty();
int rssi = wifi::global_wifi_component->wifi_rssi();
std::string registration = "{";
registration += "\"device_id\":\"${device_id}\",";
registration += "\"ip_address\":\"" + ip + "\",";
registration += "\"mac_address\":\"" + mac + "\",";
registration += "\"firmware_version\":\"${firmware_version}\",";
registration += "\"ota_readiness_topic\":\"devices/${device_id}/ready\",";
registration += "\"ota_mode_topic\":\"devices/${device_id}/ota-mode\",";
registration += "\"uses_deep_sleep\":false,";
registration += "\"ota_port\":3232,"; // Optional: omit to use default from config
registration += "\"rssi\":" + std::to_string(rssi);
registration += "}";
// Publish registration to OTA service
// Topic must match registration_topic in OTA service config.yaml
id(mqtt_client).publish("ota-service/registration", registration);
ESP_LOGI("ota", "Device registered with OTA service");1. Device Registration (Device → Service)
Topic: ota-service/registration
Payload: {
"device_id": "device-kitchen",
"ip_address": "192.168.1.100",
"mac_address": "AA:BB:CC:DD:EE:FF",
"firmware_version": "1.0.0",
"ota_readiness_topic": "devices/device-kitchen/ready",
"ota_mode_topic": "devices/device-kitchen/ota-mode",
"uses_deep_sleep": false,
"ota_port": 3232,
"rssi": -45
}2. Update Notification (Service → Device)
Topic: devices/device-kitchen/ota-mode
Payload: NEW-FIRMWARE-VERSION
3. Readiness Confirmation (Device → Service)
Topic: devices/device-kitchen/ready
Payload: OTA-READY
4. Firmware Upload (Service → Device)
- Service connects directly to device IP address on port 3232
- Uses ESPHome OTA Protocol v2 over TCP (not MQTT)
- See doc/ota/OTA_PROTOCOL.md for protocol details
- Registration topic must match
mqtt.registration_topicin OTA service config.yaml (default:ota-service/registration) - Device topics use pattern
devices/<device_id>/ota-modeanddevices/<device_id>/ready - OTA password must be identical in both ESPHome and OTA service configurations
- OTA port defaults to 3232; can be customized per-device if needed
- Devices should re-register after reboot to update IP address and firmware version
For deep-sleep devices, see esphome-examples/esp32-deep-sleep.yaml for configuration that handles intermittent connectivity.
- Port: 3232 (TCP)
- Magic Bytes:
[0x6C, 0x26, 0xF7, 0x5C, 0x45] - Chunk Size: 1024 bytes
- Acknowledgments: Every 8192 bytes (8 chunks)
- Authentication: MD5 or SHA256 with hex string concatenation
- Checksum: Actual MD5 verification of firmware data
- Timeout: 30 seconds (configurable) on all network operations
Success (0x40-0x47):
0x40HeaderOk,0x41AuthOk,0x42UpdatePrepareOk,0x43BinaryMd5Ok0x44ReceiveOk,0x45UpdateEndOk,0x46SupportsCompression,0x47ChunkOk
Errors (0x80-0x8C, 0xFF):
0x82ErrorAuthInvalid (password mismatch)0x8BErrorMd5Mismatch (checksum verification failed)0x89ErrorNotEnoughSpace (firmware too large)- See doc/ota/OTA_PROTOCOL.md for complete list
The service sends push notifications via Pushover for:
- Success (Normal priority): Firmware successfully uploaded with device details
- Failure (High priority): Upload failed with error details
- New Device (Low priority): New device registered with IP and firmware version
- Startup (Low priority): Service started successfully
- Startup Error (High priority): Service failed to start with error details
See PUSHOVER.md for complete notification documentation.
The OTA Service can automatically register itself in Home Assistant using MQTT discovery:
- Automatic Device Creation - No manual configuration needed
- Real-time Monitoring - Device count, updates available, active updates
- Service Status - Binary sensor shows if service is online
- Dashboard Ready - Create cards to monitor firmware deployment
Add to your config.yaml:
home_assistant:
enabled: true
discovery_prefix: "homeassistant"
node_id: "ota_service"
device_name: "OTA Service"
update_interval: 60Once enabled, the OTA Service appears as a device with:
- Device Count sensor
- Updates Available sensor
- Devices Updating sensor
- Last Check timestamp
- Successful Updates counter (since restart)
- Failed Updates counter (since restart)
- Service Status binary sensor
See doc/HOME_ASSISTANT.md for complete integration guide, dashboard examples, and troubleshooting.
The service maintains a SQLite database with two tables:
- Device ID (primary key), IP address, MAC address, firmware version
- Last update timestamp
- MQTT topics (readiness, OTA mode)
- Deep sleep mode configuration
- OTA port (optional, uses default if not specified)
- Update state (idle, new_version_available_transmitted, ota_transmit)
- Statistics: fail_count (failed OTA attempts), update_count (successful OTA updates)
- WiFi signal strength (rssi) in dBm
- Upload attempt records with SUCCESS/FAIL state
- Device ID, version, attempt timestamp
- Used for tracking firmware deployment history
ota-service/
├── src/
│ ├── main.rs # Application entry point
│ ├── config.rs # Configuration management
│ ├── database.rs # SQLite device tracking
│ ├── firmware.rs # Firmware file management
│ ├── home_assistant.rs # Home Assistant MQTT discovery
│ ├── mqtt.rs # MQTT message handling
│ ├── mqtt_client.rs # MQTT client wrapper
│ ├── ota_client.rs # ESPHome OTA protocol v2 implementation
│ ├── pushover.rs # Pushover notification client
│ ├── service.rs # OTA service coordinator
│ └── web.rs # Web server and API
├── static/
│ └── index.html # Web interface
├── doc/
│ ├── HOME_ASSISTANT.md # Home Assistant integration guide
│ ├── WEB_INTERFACE.md # Web interface documentation
│ ├── SERVICE_INSTALL.md # Installation guide
│ └── ota/ # OTA protocol documentation
│ ├── README.md # Documentation index
│ ├── OTA_PROTOCOL.md # Protocol specification
│ ├── OTA_IMPLEMENTATION.md # Implementation guide
│ └── ...
├── Cargo.toml # Rust dependencies
└── README.md # This file
Comprehensive documentation is available in the doc/ directory:
Installation & Configuration:
- doc/SERVICE_INSTALL.md - Complete installation guide for Linux systemd
Web Interface:
- doc/WEB_INTERFACE.md - Web interface features and usage guide
Integrations:
- doc/HOME_ASSISTANT.md - Home Assistant MQTT discovery integration
- PUSHOVER.md - Push notification configuration
OTA Protocol:
- doc/ota/README.md - Documentation overview and index
- doc/ota/OTA_QUICK_REFERENCE.md - Quick facts and troubleshooting
- doc/ota/OTA_PROTOCOL.md - ESPHome OTA protocol v2 details
- doc/ota/OTA_IMPLEMENTATION.md - Implementation guide
- doc/ota/OTA_COMPLETE_WORKFLOW.md - End-to-end workflow
- doc/ota/EXAMPLES.rs - Code examples
Notifications:
- doc/PUSHOVER.md - Pushover notification setup
# Check device is reachable
ping 192.168.1.100
# Test OTA port
nc -zv 192.168.1.100 3232
# Check service logs
journalctl -u ota-service -f
# Verify firmware file exists and naming is correct
ls -la /var/lib/ota-service/firmware/- Verify
ota_passwordin config.yaml matches device OTA password - Password must be hexadecimal string
- Check device ESPHome configuration
- Firmware file may be corrupted
- Re-download or rebuild firmware
- Verify file integrity
- Increase timeout: Default is 30 seconds
- Check network connectivity
- Device may be in deep sleep mode
See doc/ota/OTA_QUICK_REFERENCE.md for more troubleshooting tips.
# Debug build
cargo build
# Release build
cargo build --release
# Run tests
cargo test
# Check code
cargo clippy- tokio: Async runtime
- rumqttc: MQTT client
- sqlite: Database
- reqwest: HTTP client (Pushover)
- md5, sha2: Cryptographic hashing
- serde, serde_json, serde_yaml: Serialization
- log, fern: Logging
- config: Configuration management
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch
- Make your changes with tests
- Submit a pull request
This is free software released under a dual license. You may use either the MIT license or the GNU General Public License v3 (GPLv3) at your convenience. See the LICENSE.md files in the main folder.
- ESPHome project for the OTA protocol specification
- Pushover for notification service
- Claude Sonnet V4.5 in support of this development
For issues and questions:
- Review documentation in
doc/ota/ - Check logs:
journalctl -u ota-service -f - Enable debug logging: Set
log_level: "debug"in config.yaml
Made with ❤️ for the ESPHome community
If you find this project helpful, consider buying me a coffee! ☕