diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..75d40c1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +# Log files +logs/ + +# ESPHome secrets +secrets.yaml + + +# ESPHome build cache +.esphome/ + +# IDE +.vscode/ +.idea/ diff --git a/CONFIGURATION_GUIDE.md b/CONFIGURATION_GUIDE.md new file mode 100644 index 0000000..2f1a8a7 --- /dev/null +++ b/CONFIGURATION_GUIDE.md @@ -0,0 +1,537 @@ +# Neato Connected - Configuration Guide + +This project uses a modular configuration structure (`neato-esp-modular.yaml`), making it easy to: +- Switch between ESP32-C3, ESP32, and ESP8266 boards with one line +- Enable/disable optional features (syslog, debug tools) +- Customize board configurations without editing the main file + +## Prerequisites + +**Required (choose one):** + +**Option A: Docker** (Recommended - works on all platforms) +- All ESPHome commands run in Docker containers +- Install from: https://www.docker.com/get-started + - macOS/Windows: Docker Desktop + - Linux: Docker Engine +- Verify installation: `docker --version` +- **Use this option if:** You want isolated environment, no Python setup needed + +**Option B: Native ESPHome Installation** +- Install ESPHome via pip: `pip3 install esphome` +- Requires Python 3.9+ +- Verify installation: `esphome version` +- **Use this option if:** You prefer native tools, already have Python environment +- **Note:** If using native ESPHome, run commands directly (e.g., `esphome run neato-esp-modular.yaml`) instead of using the Docker wrapper scripts + +**Optional:** +- Git (for cloning the repository) +- Text editor (VS Code, Sublime Text, etc.) + +## Choose Your Script + +### Using Docker (Recommended) + +This project includes **two helper scripts** that wrap Docker commands for different operating systems: + +| Operating System | Script to Use | How to Run | +|-----------------|---------------|------------| +| **Windows** | `esphome.ps1` | `.\esphome.ps1 usb-run` | +| **macOS / Linux** | `esphome.sh` | `./esphome.sh usb-run` | +| **Windows (Git Bash/WSL)** | `esphome.sh` | `./esphome.sh usb-run` | + +**Notes:** +- Both scripts support either Docker or native ESPHome installations +- Scripts will auto-detect and prefer Docker if both are installed +- **Windows users:** PowerShell script (`esphome.ps1`) is recommended for native Windows support +- You can use `esphome.sh` on Windows if you have Git Bash or WSL installed + +### Using Native ESPHome + +If you installed ESPHome via pip, you can also use the helper scripts (they will auto-detect native ESPHome), or run commands directly: + +```bash +# First-time setup (USB/Serial) - specify device port +esphome run neato-esp-modular.yaml --device /dev/cu.usbserial-0001 # macOS/Linux +esphome run neato-esp-modular.yaml --device COM3 # Windows + +# Subsequent updates (OTA) - auto-detect over WiFi +esphome run neato-esp-modular.yaml + +# Just compile +esphome compile neato-esp-modular.yaml + +# View logs (USB) +esphome logs neato-esp-modular.yaml --device /dev/cu.usbserial-0001 # macOS/Linux +esphome logs neato-esp-modular.yaml --device COM3 # Windows + +# View logs (WiFi) +esphome logs neato-esp-modular.yaml + +# Validate config +esphome config neato-esp-modular.yaml +``` + +**Note:** +- For first-time USB upload, you must specify `--device` with the serial port +- After initial setup, ESPHome will auto-detect the device over WiFi for OTA updates +- The helper scripts (`esphome.sh` and `esphome.ps1`) support native ESPHome and provide USB port auto-detection + +## Quick Start + +### 1. Set Up Secrets + +```bash +cp secrets.yaml.example secrets.yaml +``` + +Edit `secrets.yaml` with your actual values: +```yaml +wifi_ssid: "YourNetworkName" +wifi_password: "YourWiFiPassword" +api_encryption_key: "your-base64-key" +ota_password: "your-ota-password" + +# Optional: Only needed if enabling optional features +syslog_server: "192.168.1.100" # For syslog feature +captive_portal_password: "fallback-ap-pwd" # For captive portal feature +``` + +### 2. Select Your Board + +Edit `neato-esp-modular.yaml` and uncomment your board type: + +```yaml +packages: + board: !include boards/esp32c3.yaml # ESP32-C3 (recommended) + # board: !include boards/esp32.yaml # Standard ESP32 + # board: !include boards/esp8266.yaml # ESP8266 +``` + +**Board options:** +- **ESP32-C3**: Recommended for new builds (ESP-IDF framework) +- **ESP32**: Standard ESP32 boards (ESP-IDF framework) +- **ESP8266**: Older boards like NodeMCU (Arduino framework) + +See [Board Selection](#selecting-your-board) for GPIO pin details. + +### 3. Compile and Upload + +**First-time setup (USB/Serial):** +```bash +# macOS/Linux - Auto-detect USB port and upload: +./esphome.sh usb-run + +# OR specify port manually if auto-detect fails: +./esphome.sh usb-run /dev/cu.usbserial-0001 # macOS/Linux + +# Windows - Use PowerShell script: +.\esphome.ps1 usb-run # Auto-detect +.\esphome.ps1 usb-run COM3 # Manual port +``` + +**Subsequent updates (OTA - Over The Air):** +```bash +# macOS/Linux: +./esphome.sh run # Compile and upload +./esphome.sh compile && ./esphome.sh upload # Two-step + +# Windows PowerShell: +.\esphome.ps1 run # Compile and upload +.\esphome.ps1 compile; .\esphome.ps1 upload # Two-step +``` + +**Just compile (no upload):** +```bash +./esphome.sh compile # macOS/Linux +.\esphome.ps1 compile # Windows +``` + +## Modular Configuration Setup + +The modular configuration separates board-specific settings from optional features. + +### Benefits + +- **Clean separation**: Board configs, features, and core logic are in separate files +- **Easy board switching**: Change one line to switch between ESP32-C3, ESP32, or ESP8266 +- **Optional features**: Enable/disable syslog and debug tools as needed +- **Production-ready default**: Both optional features disabled by default for minimal configuration +- **Easy customization**: Add your own board configs or feature modules + +### File Structure + +``` +neato-connected/ +├── neato-esp-modular.yaml # Main config (edit this) +├── boards/ +│ ├── esp32c3.yaml # ESP32-C3 configuration +│ ├── esp32.yaml # Standard ESP32 configuration +│ └── esp8266.yaml # ESP8266 configuration +├── features/ +│ ├── syslog.yaml # Optional: Remote logging +│ └── debug-tools.yaml # Optional: Debug buttons +└── secrets.yaml # Your private settings +``` + +### Selecting Your Board + +Edit `neato-esp-modular.yaml` and uncomment ONE board in the packages section: + +```yaml +packages: + board: !include boards/esp32c3.yaml + # board: !include boards/esp32.yaml + # board: !include boards/esp8266.yaml +``` + +### Board-Specific GPIO Pins + +Each board configuration includes appropriate UART pins: + +| Board Type | TX Pin | RX Pin | Framework | Notes | +|------------|--------|--------|-----------|-------| +| ESP32-C3 | GPIO21 | GPIO20 | ESP-IDF | Default for ESP32-C3-DevKitM-1 | +| ESP32 | GPIO17 | GPIO16 | ESP-IDF | Standard ESP32-DevKitC | +| ESP8266 | GPIO1 | GPIO3 | Arduino | NodeMCU, Wemos D1 Mini | + +**Framework Notes:** +- **ESP32 boards use ESP-IDF**: Lower-level framework with better performance and features +- **ESP8266 uses Arduino**: Standard framework for ESP8266 compatibility + +**To use different pins:** Edit the appropriate file in `boards/` directory. + +### Enabling/Disabling Optional Features + +**Note:** All optional features are **disabled by default** for a minimal, production-ready configuration. + +#### Captive Portal (WiFi Fallback AP) + +**Status:** Disabled by default + +**What it does:** Creates a fallback WiFi access point if the device cannot connect to your configured WiFi. This makes initial setup easier - if WiFi credentials are wrong, the device creates its own AP that you can connect to for reconfiguration. + +**Enable:** Uncomment in the packages section of `neato-esp-modular.yaml`: +```yaml +captive_portal: !include + file: features/captive-portal.yaml + optional: true +``` + +**Configure:** Set the fallback AP password in `secrets.yaml`: +```yaml +captive_portal_password: "your-fallback-ap-password" +``` + +**Disable:** Comment out the captive portal package (default): +```yaml +# captive_portal: !include +# file: features/captive-portal.yaml +# optional: true +``` + +**When to enable:** +- During initial setup for easier WiFi configuration +- If you frequently change WiFi networks +- For troubleshooting WiFi connectivity issues + +**Note:** The fallback AP will be named "[Your Device Name] Fallback" (e.g., "Neato Robot Fallback"). + +#### Syslog (Remote Logging) + +**Status:** Disabled by default + +**Enable:** Uncomment in the packages section of `neato-esp-modular.yaml`: +```yaml +syslog: !include + file: features/syslog.yaml + optional: true +``` + +**Configure:** Set your syslog server IP in `secrets.yaml`: +```yaml +syslog_server: "192.168.1.100" # Your syslog server IP +``` + +**Disable:** Comment out the syslog package (default): +```yaml +# syslog: !include +# file: features/syslog.yaml +# optional: true +``` + +#### Debug Tools + +**Status:** Disabled by default (only needed for development/troubleshooting) + +**Enable:** Uncomment in the packages section of `neato-esp-modular.yaml`: +```yaml +debug: !include + file: features/debug-tools.yaml + optional: true +``` + +**Disable:** Comment out the debug package (default): +```yaml +# debug: !include +# file: features/debug-tools.yaml +# optional: true +``` + +Debug tools include: +- Text input entity for sending custom commands +- "DEBUG: Send Command" button +- "DEBUG: Quick Test" button (runs GetErr, GetState, GetCharger, GetVersion, GetUserSettings, GetWarranty, TestMode, GetButtons, GetMotors, GetAnalogSensors, GetDigitalSensors) + +**When to enable:** +- During initial setup to test communication +- When troubleshooting robot commands +- When exploring undocumented commands + +## Custom Board Configuration + +### Creating a Custom Board Config + +1. Copy an existing board file: + ```bash + cp boards/esp32.yaml boards/my-custom-board.yaml + ``` + +2. Edit the GPIO pins and board type: + ```yaml + substitutions: + uart_tx: "GPIO25" # Your TX pin + uart_rx: "GPIO26" # Your RX pin + + esp32: + board: your-board-name + framework: + type: esp-idf # Use esp-idf for ESP32, arduino for ESP8266 + ``` + +3. Use it in `neato-esp-modular.yaml`: + ```yaml + packages: + board: !include boards/my-custom-board.yaml + ``` + +## Script Selection (Windows vs macOS/Linux) + +This project includes two helper scripts: +- **`esphome.sh`** - For macOS/Linux (bash script) +- **`esphome.ps1`** - For Windows (PowerShell script) + +### Windows Users + +Windows cannot run `.sh` bash scripts natively. Choose one of these options: + +**Option A: Use PowerShell script (Recommended)** +```powershell +.\esphome.ps1 usb-run +``` + +**Option B: Use Git Bash** +If you have Git for Windows installed: +```bash +./esphome.sh usb-run +``` + +**Option C: Use WSL (Windows Subsystem for Linux)** +If you have WSL installed: +```bash +./esphome.sh usb-run +``` + +### macOS/Linux Users + +Simply use the bash script: +```bash +./esphome.sh usb-run +``` + +## Initial USB Setup + +### Hardware Connection + +1. **Connect ESP32/ESP8266 to your computer via USB** +2. **Identify the serial port:** + - macOS: Usually `/dev/cu.usbserial-*` or `/dev/cu.SLAB_USBtoUART` + - Linux: Usually `/dev/ttyUSB0` or `/dev/ttyACM0` + - Windows: Usually `COM3`, `COM4`, etc. + +### First Upload (USB/Serial) + +**For the first upload, you MUST use USB/Serial since OTA isn't configured yet.** + +**Recommended - Auto-detect USB port:** +```bash +./esphome.sh usb-run +``` + +The script will automatically detect your USB serial port and upload the firmware. + +**If auto-detection fails, specify the port manually:** + +First, find your serial port: +```bash +# macOS +ls /dev/cu.* + +# Linux +ls /dev/ttyUSB* /dev/ttyACM* + +# Windows (PowerShell) +[System.IO.Ports.SerialPort]::getportnames() + +# Windows (Device Manager) +# Open Device Manager and check "Ports (COM & LPT)" +``` + +Then use the specific port: +```bash +./esphome.sh usb-run /dev/cu.usbserial-0001 # macOS/Linux +./esphome.sh usb-run COM3 # Windows +``` + +This will: +1. Compile the configuration +2. Upload the firmware via USB to the specified port +3. Connect to the device and show live logs + +### After First Upload + +Once the device is running and connected to WiFi: + +1. **Find the device IP** - Check your router or look in the logs +2. **Use OTA for updates:** + ```bash + ./esphome.sh upload + ``` +3. **ESPHome will detect the device on your network and upload wirelessly** + +### Monitoring Logs + +**Via USB:** +```bash +# Auto-detect USB port: +./esphome.sh usb-logs + +# OR specify port manually: +./esphome.sh usb-logs /dev/cu.usbserial-0001 # macOS/Linux +./esphome.sh usb-logs COM3 # Windows +``` + +**Via WiFi (after first upload):** +```bash +./esphome.sh logs +``` + +## Troubleshooting + +### Validation Errors + +```bash +./esphome.sh validate +``` + +### Common Issues + +#### Docker Problems + +**"docker: command not found" or "Cannot connect to Docker daemon":** +1. Ensure Docker is installed: `docker --version` +2. Start Docker Desktop (macOS/Windows) or Docker service (Linux): + ```bash + # Linux + sudo systemctl start docker + sudo systemctl enable docker + ``` +3. Check Docker is running: `docker ps` +4. Add your user to docker group (Linux): + ```bash + sudo usermod -aG docker $USER + ``` + Then log out and back in. + +**"permission denied while trying to connect to Docker daemon":** +- Make sure Docker Desktop is running (macOS/Windows) +- On Linux, ensure your user is in the `docker` group (see above) + +#### USB Upload Problems + +**"Serial port not found" or "Could not open port":** +1. Check USB cable (use a data cable, not charge-only) +2. Install USB drivers: + - ESP32-C3: Usually works with built-in drivers (uses native USB) + - ESP32/ESP8266 with CP2102: [Silicon Labs drivers](https://www.silabs.com/developers/usb-to-uart-bridge-vcp-drivers) + - ESP32/ESP8266 with CH340: [CH340 drivers](https://sparks.gogo.co.nz/ch340.html) +3. Check device permissions (Linux): + ```bash + sudo usermod -a -G dialout $USER + ``` + Then log out and back in. + +**"Failed to connect to ESP32":** +- Hold the BOOT button while uploading (some boards require this) +- Try lowering baud rate in upload by editing the config temporarily +- Ensure the board is not connected to the Neato robot during first upload + +**"A fatal error occurred: Timed out waiting for packet header":** +- Press and hold the BOOT button +- Press the RESET button briefly +- Release BOOT button +- Try upload again + +#### Runtime Issues + +**"GPIO not found"**: Your board configuration may be using the wrong pin numbering. All boards should use "GPIOxx" format (e.g., "GPIO21"). + +**"WiFi not connecting"**: +- Check your `secrets.yaml` file has the correct SSID and password +- Ensure your WiFi is 2.4GHz (ESP8266/ESP32 don't support 5GHz) +- Check WiFi credentials don't have special characters that need escaping + +**"Syslog not working"**: Verify the syslog server IP is correct in `secrets.yaml`. + +**"Can't find device for OTA upload"**: +- Check the device is connected to WiFi (view USB logs) +- Ensure your computer and ESP device are on the same network +- Try using the device's IP address directly: + ```bash + docker run --rm -v "${PWD}":/config -it esphome/esphome upload /config/neato-esp-modular.yaml --device 192.168.1.100 + ``` + +## Advanced Configuration + +### Multiple Robots + +Create different configuration files for each robot: + +```bash +cp neato-esp-modular.yaml neato-living-room.yaml +cp neato-esp-modular.yaml neato-bedroom.yaml +``` + +Edit each with different device names: +```yaml +substitutions: + device_name: neato-living-room + friendly_name: "Living Room Neato" +``` + +Compile separately: +```bash +docker run --rm -v "${PWD}":/config -it esphome/esphome compile /config/neato-living-room.yaml +``` + +## Documentation + +- [Neato Firmware Protocol Documentation](NEATO_FIRMWARE_DOCUMENTATION.md) +- [ESPHome Documentation](https://esphome.io/) + +## Support + +For issues and questions: +- Check the documentation above +- Review existing GitHub issues +- Create a new issue with your configuration and error logs diff --git a/boards/esp32.yaml b/boards/esp32.yaml new file mode 100644 index 0000000..09a7717 --- /dev/null +++ b/boards/esp32.yaml @@ -0,0 +1,11 @@ +# Standard ESP32 Board Configuration +# Use this for regular ESP32 boards (ESP32-DevKitC, NodeMCU-32S, etc.) + +substitutions: + uart_tx: "GPIO17" + uart_rx: "GPIO16" + +esp32: + board: esp32dev + framework: + type: esp-idf diff --git a/boards/esp32c3.yaml b/boards/esp32c3.yaml new file mode 100644 index 0000000..5a7dff6 --- /dev/null +++ b/boards/esp32c3.yaml @@ -0,0 +1,11 @@ +# ESP32-C3 Board Configuration +# Use this for ESP32-C3 based boards (like ESP32-C3-DevKitM-1) + +substitutions: + uart_tx: "GPIO21" + uart_rx: "GPIO20" + +esp32: + board: esp32-c3-devkitm-1 + framework: + type: esp-idf diff --git a/boards/esp8266.yaml b/boards/esp8266.yaml new file mode 100644 index 0000000..4e9730b --- /dev/null +++ b/boards/esp8266.yaml @@ -0,0 +1,11 @@ +# ESP8266 Board Configuration +# Use this for ESP8266 boards (NodeMCU, Wemos D1 Mini, etc.) + +substitutions: + uart_tx: "GPIO1" + uart_rx: "GPIO3" + +esp8266: + board: nodemcuv2 + framework: + version: recommended diff --git a/esphome.ps1 b/esphome.ps1 new file mode 100644 index 0000000..df2f76a --- /dev/null +++ b/esphome.ps1 @@ -0,0 +1,202 @@ +# ESPHome CLI helper script for Windows PowerShell +# Supports both Docker and native ESPHome installations +# Usage: .\esphome.ps1 [command] [options] + +$DOCKER_IMAGE = "esphome/esphome" +$CONFIG_FILE = "neato-esp-modular.yaml" + +# Detect whether to use Docker or native ESPHome +$USE_DOCKER = $false +$USE_NATIVE = $false + +# Check if Docker is available and running +if (Get-Command docker -ErrorAction SilentlyContinue) { + try { + docker ps *>$null + if ($LASTEXITCODE -eq 0) { + $USE_DOCKER = $true + } + } catch { + # Docker not running + } +} + +# Check if native ESPHome is available +if (Get-Command esphome -ErrorAction SilentlyContinue) { + $USE_NATIVE = $true +} + +# Prefer Docker if both are available, otherwise use native +if ($USE_DOCKER) { + $USE_NATIVE = $false +} elseif (-not $USE_NATIVE) { + Write-Host "Error: Neither Docker nor native ESPHome found." -ForegroundColor Red + Write-Host "" + Write-Host "Please install one of the following:" + Write-Host " - Docker: https://www.docker.com/get-started" + Write-Host " - Native ESPHome: pip3 install esphome" + exit 1 +} + +# Function to detect USB serial port +function Detect-USBPort { + try { + $ports = [System.IO.Ports.SerialPort]::getportnames() + if ($ports.Count -gt 0) { + return $ports[0] + } + } catch { + # SerialPort class not available + } + + Write-Host "Error: No USB serial port detected." -ForegroundColor Red + Write-Host "" + Write-Host "Please specify the port manually:" + Write-Host " .\esphome.ps1 usb-run COM3" + Write-Host " .\esphome.ps1 usb-logs COM3" + Write-Host "" + Write-Host "To find your COM port:" + Write-Host " 1. Open Device Manager" + Write-Host " 2. Look under 'Ports (COM & LPT)'" + Write-Host " 3. Find your USB Serial device (e.g., COM3, COM4)" + Write-Host "" + Write-Host "Or run this PowerShell command:" + Write-Host " [System.IO.Ports.SerialPort]::getportnames()" + Write-Host "" + return $null +} + +# Get the command +$command = $args[0] + +switch ($command) { + "compile" { + Write-Host "Compiling $CONFIG_FILE..." + if ($USE_DOCKER) { + docker run --rm -v "${PWD}:/config" -it $DOCKER_IMAGE compile /config/$CONFIG_FILE + } else { + esphome compile $CONFIG_FILE + } + } + + "upload" { + Write-Host "Uploading to $CONFIG_FILE (OTA)..." + if ($USE_DOCKER) { + docker run --rm --network=host -v "${PWD}:/config" -it $DOCKER_IMAGE upload /config/$CONFIG_FILE + } else { + esphome upload $CONFIG_FILE + } + } + + "run" { + Write-Host "Compiling and uploading $CONFIG_FILE (OTA)..." + if ($USE_DOCKER) { + docker run --rm --network=host -v "${PWD}:/config" -it $DOCKER_IMAGE run /config/$CONFIG_FILE + } else { + esphome run $CONFIG_FILE + } + } + + "usb-run" { + if ($args.Count -lt 2) { + Write-Host "Detecting USB serial port..." + $port = Detect-USBPort + if ($null -eq $port) { + exit 1 + } + Write-Host "Using auto-detected port: $port" + } else { + $port = $args[1] + Write-Host "Using specified port: $port" + } + + Write-Host "Compiling and uploading $CONFIG_FILE via USB..." + if ($USE_DOCKER) { + docker run --rm -v "${PWD}:/config" --device="$port" -it $DOCKER_IMAGE run /config/$CONFIG_FILE + } else { + esphome run $CONFIG_FILE --device $port + } + } + + "usb-logs" { + if ($args.Count -lt 2) { + Write-Host "Detecting USB serial port..." + $port = Detect-USBPort + if ($null -eq $port) { + exit 1 + } + Write-Host "Using auto-detected port: $port" + } else { + $port = $args[1] + Write-Host "Using specified port: $port" + } + + Write-Host "Showing logs via USB from $port..." + if ($USE_DOCKER) { + docker run --rm -v "${PWD}:/config" --device="$port" -it $DOCKER_IMAGE logs /config/$CONFIG_FILE + } else { + esphome logs $CONFIG_FILE --device $port + } + } + + "logs" { + Write-Host "Showing logs for $CONFIG_FILE (network)..." + if ($USE_DOCKER) { + docker run --rm --network=host -v "${PWD}:/config" -it $DOCKER_IMAGE logs /config/$CONFIG_FILE + } else { + esphome logs $CONFIG_FILE + } + } + + "validate" { + Write-Host "Validating $CONFIG_FILE..." + if ($USE_DOCKER) { + docker run --rm -v "${PWD}:/config" -it $DOCKER_IMAGE config /config/$CONFIG_FILE + } else { + esphome config $CONFIG_FILE + } + } + + "clean" { + Write-Host "Cleaning build files..." + if ($USE_DOCKER) { + docker run --rm -v "${PWD}:/config" -it $DOCKER_IMAGE clean /config/$CONFIG_FILE + } else { + esphome clean $CONFIG_FILE + } + } + + default { + Write-Host "ESPHome CLI Helper (PowerShell)" -ForegroundColor Cyan + if ($USE_DOCKER) { + Write-Host "(Using Docker: $DOCKER_IMAGE)" + } else { + Write-Host "(Using native ESPHome)" + } + Write-Host "" + Write-Host "Usage: .\esphome.ps1 [command] [options]" + Write-Host "" + Write-Host "OTA Commands (wireless, after first USB upload):" + Write-Host " compile - Compile the firmware only" + Write-Host " upload - Upload compiled firmware via OTA" + Write-Host " run - Compile and upload via OTA in one step" + Write-Host " logs - View device logs via network" + Write-Host "" + Write-Host "USB Commands (for first-time setup):" + Write-Host " usb-run - Compile and upload via USB (auto-detect port)" + Write-Host " usb-run PORT - Compile and upload via USB to specified port" + Write-Host " usb-logs - View device logs via USB (auto-detect port)" + Write-Host " usb-logs PORT - View device logs via USB from specified port" + Write-Host "" + Write-Host "Other Commands:" + Write-Host " validate - Validate configuration" + Write-Host " clean - Clean build files" + Write-Host "" + Write-Host "Examples:" + Write-Host " .\esphome.ps1 usb-run # First upload (auto-detect)" + Write-Host " .\esphome.ps1 usb-run COM3 # First upload (manual port)" + Write-Host " .\esphome.ps1 run # Subsequent OTA updates" + Write-Host "" + exit 1 + } +} diff --git a/esphome.sh b/esphome.sh new file mode 100755 index 0000000..9029844 --- /dev/null +++ b/esphome.sh @@ -0,0 +1,231 @@ +#!/bin/bash +# ESPHome CLI helper script +# Supports both Docker and native ESPHome installations + +DOCKER_IMAGE="esphome/esphome" +CONFIG_FILE="neato-esp-modular.yaml" + +# Detect whether to use Docker or native ESPHome +USE_DOCKER=false +USE_NATIVE=false + +if command -v docker &> /dev/null; then + if docker ps &> /dev/null; then + USE_DOCKER=true + fi +fi + +if command -v esphome &> /dev/null; then + USE_NATIVE=true +fi + +# Prefer Docker if both are available, otherwise use native +if [ "$USE_DOCKER" = true ]; then + USE_NATIVE=false +elif [ "$USE_NATIVE" = false ]; then + echo "Error: Neither Docker nor native ESPHome found." >&2 + echo "" >&2 + echo "Please install one of the following:" >&2 + echo " - Docker: https://www.docker.com/get-started" >&2 + echo " - Native ESPHome: pip3 install esphome" >&2 + exit 1 +fi + +# Function to detect USB serial port +detect_usb_port() { + # Try to find common USB serial ports + if [[ "$OSTYPE" == "darwin"* ]]; then + # macOS - look for common USB serial adapters + PORT=$(ls /dev/cu.* 2>/dev/null | grep -E "(usbserial|SLAB|wchusbserial)" | head -n 1) + elif [[ "$OSTYPE" == "linux-gnu"* ]] || [[ "$OSTYPE" == "linux" ]]; then + # Linux - look for USB and ACM devices + PORT=$(ls /dev/ttyUSB* /dev/ttyACM* 2>/dev/null | head -n 1) + elif [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]] || [[ "$OSTYPE" == "win32" ]]; then + # Windows (Git Bash, WSL, Cygwin) - look for COM ports + # Try to detect COM ports (this is tricky on Windows) + for i in {3..20}; do + if [ -e "/dev/ttyS$((i-1))" ]; then + PORT="COM$i" + break + fi + done + fi + + if [ -z "$PORT" ]; then + echo "Error: No USB serial port detected." >&2 + echo "" >&2 + echo "Please specify the port manually:" >&2 + if [[ "$OSTYPE" == "darwin"* ]]; then + echo " ./esphome.sh usb-run /dev/cu.usbserial-0001" >&2 + echo " ./esphome.sh usb-logs /dev/cu.usbserial-0001" >&2 + elif [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]] || [[ "$OSTYPE" == "win32" ]]; then + echo " ./esphome.sh usb-run COM3" >&2 + echo " ./esphome.sh usb-logs COM3" >&2 + else + echo " ./esphome.sh usb-run /dev/ttyUSB0" >&2 + echo " ./esphome.sh usb-logs /dev/ttyUSB0" >&2 + fi + echo "" >&2 + echo "Available ports:" >&2 + if [[ "$OSTYPE" == "darwin"* ]]; then + # Show all cu devices on macOS (excluding Bluetooth and debug console) + PORTS=$(ls /dev/cu.* 2>/dev/null | grep -v -E "(Bluetooth-Incoming-Port|debug-console)") + if [ -n "$PORTS" ]; then + echo "$PORTS" | sed 's/^/ /' >&2 + else + echo " No USB serial ports found (only Bluetooth/system ports detected)" >&2 + echo " Make sure your ESP device is connected via USB" >&2 + fi + elif [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]] || [[ "$OSTYPE" == "win32" ]]; then + # Windows - suggest using Device Manager + echo " On Windows, check Device Manager for COM port numbers" >&2 + echo " (Ports (COM & LPT) section)" >&2 + echo "" >&2 + echo " Or use PowerShell to list ports:" >&2 + echo " [System.IO.Ports.SerialPort]::getportnames()" >&2 + else + # Linux - show USB and ACM devices + if ls /dev/ttyUSB* /dev/ttyACM* 2>/dev/null 1>&2; then + ls /dev/ttyUSB* /dev/ttyACM* 2>/dev/null | sed 's/^/ /' >&2 + else + echo " No USB serial ports found" >&2 + fi + fi + return 1 + fi + + echo "$PORT" + return 0 +} + +case "$1" in + compile) + echo "Compiling $CONFIG_FILE..." + if [ "$USE_DOCKER" = true ]; then + docker run --rm -v "${PWD}":/config -it $DOCKER_IMAGE compile /config/$CONFIG_FILE + else + esphome compile $CONFIG_FILE + fi + ;; + + upload) + echo "Uploading to $CONFIG_FILE (OTA)..." + if [ "$USE_DOCKER" = true ]; then + docker run --rm --network=host -v "${PWD}":/config -it $DOCKER_IMAGE upload /config/$CONFIG_FILE + else + esphome upload $CONFIG_FILE + fi + ;; + + run) + echo "Compiling and uploading $CONFIG_FILE (OTA)..." + if [ "$USE_DOCKER" = true ]; then + docker run --rm --network=host -v "${PWD}":/config -it $DOCKER_IMAGE run /config/$CONFIG_FILE + else + esphome run $CONFIG_FILE + fi + ;; + + usb-run) + if [ -z "$2" ]; then + PORT=$(detect_usb_port) + if [ $? -ne 0 ]; then + exit 1 + fi + echo "Using auto-detected port: $PORT" + else + PORT="$2" + echo "Using specified port: $PORT" + fi + + echo "Compiling and uploading $CONFIG_FILE via USB..." + if [ "$USE_DOCKER" = true ]; then + docker run --rm -v "${PWD}":/config --device="$PORT" -it $DOCKER_IMAGE run /config/$CONFIG_FILE + else + esphome run $CONFIG_FILE --device $PORT + fi + ;; + + usb-logs) + if [ -z "$2" ]; then + PORT=$(detect_usb_port) + if [ $? -ne 0 ]; then + exit 1 + fi + echo "Using auto-detected port: $PORT" + else + PORT="$2" + echo "Using specified port: $PORT" + fi + + echo "Showing logs via USB from $PORT..." + if [ "$USE_DOCKER" = true ]; then + docker run --rm -v "${PWD}":/config --device="$PORT" -it $DOCKER_IMAGE logs /config/$CONFIG_FILE + else + esphome logs $CONFIG_FILE --device $PORT + fi + ;; + + logs) + echo "Showing logs for $CONFIG_FILE (network)..." + if [ "$USE_DOCKER" = true ]; then + docker run --rm --network=host -v "${PWD}":/config -it $DOCKER_IMAGE logs /config/$CONFIG_FILE + else + esphome logs $CONFIG_FILE + fi + ;; + + validate) + echo "Validating $CONFIG_FILE..." + if [ "$USE_DOCKER" = true ]; then + docker run --rm -v "${PWD}":/config -it $DOCKER_IMAGE config /config/$CONFIG_FILE + else + esphome config $CONFIG_FILE + fi + ;; + + clean) + echo "Cleaning build files..." + if [ "$USE_DOCKER" = true ]; then + docker run --rm -v "${PWD}":/config -it $DOCKER_IMAGE clean /config/$CONFIG_FILE + else + esphome clean $CONFIG_FILE + fi + ;; + + *) + echo "ESPHome CLI Helper" + if [ "$USE_DOCKER" = true ]; then + echo "(Using Docker: $DOCKER_IMAGE)" + else + echo "(Using native ESPHome)" + fi + echo "" + echo "Usage: ./esphome.sh [command] [options]" + echo "" + echo "OTA Commands (wireless, after first USB upload):" + echo " compile - Compile the firmware only" + echo " upload - Upload compiled firmware via OTA" + echo " run - Compile and upload via OTA in one step" + echo " logs - View device logs via network" + echo "" + echo "USB Commands (for first-time setup):" + echo " usb-run - Compile and upload via USB (auto-detect port)" + echo " usb-run PORT - Compile and upload via USB to specified port" + echo " usb-logs - View device logs via USB (auto-detect port)" + echo " usb-logs PORT - View device logs via USB from specified port" + echo "" + echo "Other Commands:" + echo " validate - Validate configuration" + echo " clean - Clean build files" + echo "" + echo "Examples:" + echo " ./esphome.sh usb-run # First upload (auto-detect)" + echo " ./esphome.sh usb-run /dev/cu.usbserial-0001 # macOS/Linux manual port" + echo " ./esphome.sh usb-run COM3 # Windows manual port" + echo " ./esphome.sh run # Subsequent OTA updates" + echo " ./esphome.sh compile && ./esphome.sh upload # Two-step OTA update" + echo "" + exit 1 + ;; +esac diff --git a/features/captive-portal.yaml b/features/captive-portal.yaml new file mode 100644 index 0000000..7d44b9c --- /dev/null +++ b/features/captive-portal.yaml @@ -0,0 +1,12 @@ +# Optional Captive Portal Feature +# Creates a fallback WiFi access point for easy initial setup +# Configure captive_portal_password in secrets.yaml + +# Create a fallback hotspot (captive portal) if WiFi connection fails +wifi: + ap: + ssid: "${friendly_name} Fallback" + password: !secret captive_portal_password + +# Enable captive portal +captive_portal: diff --git a/features/debug-tools.yaml b/features/debug-tools.yaml new file mode 100644 index 0000000..e173ef4 --- /dev/null +++ b/features/debug-tools.yaml @@ -0,0 +1,51 @@ +# Optional Debug Tools Feature +# Adds text input and buttons for testing Neato commands + +text: + - platform: template + name: "DEBUG: Command Input" + id: debug_command + optimistic: true + initial_value: "GetHelp" + mode: text + entity_category: "diagnostic" + +button: + - platform: template + name: "DEBUG: Send Command" + on_press: + - uart.write: !lambda |- + std::string cmd = id(debug_command).state; + if (cmd.empty()) { + ESP_LOGW("debug", "No command entered"); + return {}; + } + if (cmd.back() != '\n') cmd += "\n"; + return std::vector(cmd.begin(), cmd.end()); + entity_category: "diagnostic" + + - platform: template + name: "DEBUG: Quick Test" + on_press: + - uart.write: "GetErr \n" + - delay: 500ms + - uart.write: "GetState \n" + - delay: 500ms + - uart.write: "GetCharger \n" + - delay: 500ms + - uart.write: "GetVersion \n" + - delay: 500ms + - uart.write: "GetUserSettings \n" + - delay: 500ms + - uart.write: "GetWarranty \n" + - delay: 500ms + - uart.write: "TestMode \n" + - delay: 500ms + - uart.write: "GetButtons \n" + - delay: 500ms + - uart.write: "GetMotors \n" + - delay: 500ms + - uart.write: "GetAnalogSensors \n" + - delay: 500ms + - uart.write: "GetDigitalSensors \n" + entity_category: "diagnostic" diff --git a/features/diagnostics.yaml b/features/diagnostics.yaml new file mode 100644 index 0000000..2cc9493 --- /dev/null +++ b/features/diagnostics.yaml @@ -0,0 +1,16 @@ +# Optional Diagnostics Feature +# Adds sensors for monitoring device health and connectivity +# Includes WiFi signal strength and uptime tracking + +sensor: + # WiFi Signal Strength + - platform: wifi_signal + name: "WiFi Signal" + update_interval: 60s + entity_category: "diagnostic" + + # Device Uptime + - platform: uptime + name: "Uptime" + update_interval: 60s + entity_category: "diagnostic" diff --git a/features/hardware-monitoring.yaml b/features/hardware-monitoring.yaml new file mode 100644 index 0000000..2281b59 --- /dev/null +++ b/features/hardware-monitoring.yaml @@ -0,0 +1,38 @@ +# Optional Hardware Monitoring Feature +# Adds ESP32/ESP8266 hardware health sensors to Home Assistant +# Monitors memory, CPU, and performance metrics + +debug: + update_interval: 30s + +sensor: + # Free heap memory + - platform: debug + free: + name: "Free Heap" + entity_category: "diagnostic" + + # Largest free memory block + - platform: debug + block: + name: "Largest Free Block" + entity_category: "diagnostic" + + # Main loop execution time + - platform: debug + loop_time: + name: "Loop Time" + entity_category: "diagnostic" + +text_sensor: + # Device information (chip model, cores, etc.) + - platform: debug + device: + name: "Device Info" + entity_category: "diagnostic" + + # Reset reason + - platform: debug + reset_reason: + name: "Reset Reason" + entity_category: "diagnostic" diff --git a/features/prometheus.yaml b/features/prometheus.yaml new file mode 100644 index 0000000..f80e909 --- /dev/null +++ b/features/prometheus.yaml @@ -0,0 +1,13 @@ +# Optional Prometheus Metrics Feature +# Exposes metrics endpoint for Prometheus scraping +# Access via http://[device-name].local/metrics +# +# Provides built-in ESP32 hardware metrics: +# - heap_free: Free heap memory (bytes) +# - heap_max_block: Largest contiguous free memory block (bytes) +# - cpu_frequency: CPU frequency (Hz) +# - loop_time: Main loop execution time (ms) +# +# Also exposes all configured sensors, switches, and other entities + +prometheus: diff --git a/features/syslog.yaml b/features/syslog.yaml new file mode 100644 index 0000000..f9ffcef --- /dev/null +++ b/features/syslog.yaml @@ -0,0 +1,17 @@ +# Optional Syslog Feature +# Sends ESP logs to a remote syslog server (e.g., Loki/Promtail) +# Configure syslog_server in secrets.yaml + +# Required for syslog +udp: + id: udp_component + addresses: !secret syslog_server + +syslog: + udp_id: udp_component + port: 514 + strip: true + +# Disable serial logging when using syslog +logger: + baud_rate: 0 diff --git a/features/web-server.yaml b/features/web-server.yaml new file mode 100644 index 0000000..36432fe --- /dev/null +++ b/features/web-server.yaml @@ -0,0 +1,13 @@ +# Optional Web Server Feature +# Enables a web interface to monitor and control the device +# Access via http://[device-name].local or http://[ip-address] +# +# Note: OTA updates are disabled for the web server to avoid conflicts +# with the main OTA configuration. OTA updates will continue to work +# through the ESPHome API. + +web_server: + port: 80 + version: 3 + local: true + ota: false # Disable web server OTA to use only ESPHome API OTA diff --git a/neato-esp-modular.yaml b/neato-esp-modular.yaml new file mode 100644 index 0000000..95e8d54 --- /dev/null +++ b/neato-esp-modular.yaml @@ -0,0 +1,1251 @@ +############################################ +# Neato Connected - ESPHome Configuration +############################################ +# +# USER CONFIGURATION - Edit this section +# + +substitutions: + device_name: neato-esp + friendly_name: "Neato Robot" + wifi_domain: ".lan" # mDNS domain (usually .local or .lan) + + # NOTE: To change board or features, edit the packages section below + # by uncommenting the desired board and features + +############################################ +# Load Modular Configuration +############################################ + +# Load board-specific configuration +# BOARD SELECTION: Uncomment ONE of the following +packages: + board: !include boards/esp32c3.yaml + # board: !include boards/esp32.yaml + # board: !include boards/esp8266.yaml + + # OPTIONAL FEATURES: Uncomment to enable + # Captive portal - uncomment to enable WiFi fallback AP for easy setup + # captive_portal: !include + # file: features/captive-portal.yaml + # optional: true + + # Syslog - uncomment to enable remote logging + syslog: !include + file: features/syslog.yaml + optional: true + + # Debug tools - uncomment to enable command testing features + # debug: !include + # file: features/debug-tools.yaml + # optional: true + + # Web server - uncomment to enable web interface + web_server: !include + file: features/web-server.yaml + optional: true + + # Diagnostics - uncomment to enable WiFi signal and uptime sensors + diagnostics: !include + file: features/diagnostics.yaml + optional: true + + # Prometheus - uncomment to enable metrics endpoint for monitoring + # prometheus: !include + # file: features/prometheus.yaml + # optional: true + + # Hardware monitoring - uncomment to enable ESP32 hardware sensors in HA + hardware_monitoring: !include + file: features/hardware-monitoring.yaml + optional: true + +ota: + - platform: esphome + password: !secret neato_vacuum_ota + +wifi: + ssid: !secret wifi_ssid + password: !secret wifi_password + domain: ${wifi_domain} + +############################################ +# ESPHome Core Configuration +############################################ +esphome: + name: ${device_name} + comment: Neato ESP32 controller + friendly_name: ${friendly_name} +# Combatability between esphome config and this card is ensured as long as the +# major version's match, for relase candicates, betas etc (indicated by X.X-a/b/rc.X), the version +# has to match perfectly otherwise compatability is not promised. +# +# Please try to keep the versions the same, and check the github for updates! + project: + name: philip2809.neato-connected + version: "1.2-b.3" + + on_boot: + - priority: 600 + then: + - lambda: !lambda |- + id(uiState).publish_state("Starting..."); + + - priority: -100 + then: + - lambda: !lambda |- + id(uiState).publish_state("Starting..."); + + ESPTime t = ESPTime::from_epoch_local(id(g_last_cleaning_start_time)); + std::string ts = t.strftime("%Y-%m-%d %H:%M:%S"); + id(last_cleaning_time).publish_state(ts.c_str()); + + if (id(g_last_cleaning_type) == 0) { + id(last_cleaning_type).publish_state("HOUSE"); + } else if (id(g_last_cleaning_type) == 1) { + id(last_cleaning_type).publish_state("SPOT"); + } + + id(last_cleaning_duration).publish_state(id(g_last_cleaning_duration)); + + +script: + - id: get_user_settings + mode: queued + then: + - delay: 100ms + - uart.write: "GetUserSettings \n" + + - id: get_all_data + mode: queued + then: + - uart.write: "GetErr \n" + - delay: 50ms + - uart.write: "GetState \n" + - delay: 50ms + - uart.write: "GetCharger \n" + - delay: 50ms + - uart.write: "GetUserSettings \n" + - delay: 50ms + - uart.write: "GetWarranty \n" + - delay: 50ms + - uart.write: "TestMode \n" + - delay: 50ms + - uart.write: "GetVersion \n" + + - id: initial_data_get # when the robot has booted, run this initial fetch after 5 seconds of being able to send commands, the 5 sec needed to the robot can calculate battery levet etc. + mode: queued + then: + - delay: 5sec + - script.execute: get_all_data + + - id: robotShutdown + mode: queued + then: + - lambda: 'id(uiState).publish_state("Shutting down...");' + - uart.write: "TestMode On \n" + - delay: 100ms + - uart.write: "SetSystemMode Shutdown \n" + + - id: powercycle + mode: queued + then: + - lambda: 'id(initial_ready) = false;' # I want to refetch all data in case we powercycle + - lambda: 'id(uiState).publish_state("Shutting down...");' + - uart.write: "TestMode On \n" + - delay: 100ms + - uart.write: "SetSystemMode PowerCycle \n" + + - id: cleaning_stopped + then: + - lambda: !lambda |- + uint64_t now_ts = id(homeassistant_time).now().timestamp; + uint64_t start_ts = id(g_last_cleaning_start_time); + + if (start_ts > 0) { + uint32_t duration_min = (now_ts - start_ts) / 60; + id(g_last_cleaning_duration) = duration_min; + id(last_cleaning_duration).publish_state(duration_min); + } + - id: play_extra_sounds + parameters: + soundid: int + then: + - if: + condition: + lambda: |- + return id(g_play_extra_sounds); + then: + - uart.write: !lambda |- + std::string s = + "PlaySound SoundId " + std::to_string(soundid) + "\n"; + return std::vector(s.begin(), s.end()); + + - id: send_event + parameters: + event: string + then: + - uart.write: !lambda |- + std::string s = + "SetEvent event " + event + " SKey " + id(robot_skey) + "\n"; + return std::vector(s.begin(), s.end()); + + + +globals: + - id: initial_ready + type: bool + restore_value: no + initial_value: 'false' + + - id: robot_skey + type: std::string + restore_value: no + + # --- STORE IN FLASH, MAX 96 BYTES! --- + - id: g_last_cleaning_start_time # 8 BYTES + type: uint64_t + restore_value: yes + + - id: g_last_cleaning_type # 4 BYTES + type: uint32_t + restore_value: yes + + - id: g_last_cleaning_duration # 4 BYTES + type: uint32_t + restore_value: yes + + - id: g_play_extra_sounds # 1 BYTE + type: bool + restore_value: yes + # --- TOTAL BYTES USED: 17 BYTES --- + + +interval: + - interval: 2sec + then: + - uart.write: "GetErr \n" + - uart.write: "GetState \n" + - interval: 2min + then: + - uart.write: "GetCharger \n" + +api: + encryption: + key: !secret neato_vacuum_api + + actions: + - action: send_cmd + variables: + command: string + then: + - uart.write: !lambda |- + // Convert to std::string + std::string s = command; + + // IMPORTANT: keep newline exactly like YAML version + if (s.empty() || s.back() != '\n') + s += "\n"; + + // convert string → vector + return std::vector(s.begin(), s.end()); + + - action: clear_alert + variables: + alert: string + then: + - uart.write: !lambda |- + std::string cmd = std::string("SetUIError clearalert ") + alert + "\n"; + return std::vector(cmd.begin(), cmd.end()); + + - action: play_sound + variables: + soundid: int + then: + - uart.write: !lambda |- + // Create std::string + std::string s = "PlaySound SoundId " + std::to_string(soundid) + "\n"; + // convert string → vector + return std::vector(s.begin(), s.end()); + +# Logger configuration (will be overridden if syslog feature is enabled) +logger: + level: INFO + logs: + uart_parser: WARN + uart: INFO + sensor: INFO + binary_sensor: INFO + text_sensor: INFO + switch: INFO + button: INFO + script: INFO + api: WARN + ota: INFO + + +uart: + id: uart_bus + baud_rate: 115200 + tx_pin: ${uart_tx} + rx_pin: ${uart_rx} + rx_buffer_size: 65535 + debug: + dummy_receiver: true + direction: RX + after: + bytes: 0 + #timeout: 5sec + #delimiter: "\n" + delimiter: "\x1A" + sequence: + - lambda: !lambda |- + UARTDebug::log_string(direction, bytes); + + // convert bytes -> std::string + std::string str(bytes.begin(), bytes.end()); + + // ---- TRIM FUNCTION ---- + auto trim = [](std::string &s) { + // trim leading + s.erase(s.begin(), std::find_if(s.begin(), s.end(), + [](unsigned char ch){ return !std::isspace(ch); })); + // trim trailing + s.erase(std::find_if(s.rbegin(), s.rend(), + [](unsigned char ch){ return !std::isspace(ch); }).base(), s.end()); + }; + + // --- HEX DECODER --- + auto hex_to_uint32 = [](const std::string &hex) -> uint32_t { + return static_cast(strtoul(hex.c_str(), nullptr, 16)); + }; + + // ---- SPLIT ON CRLF ---- + std::vector lines; + size_t start = 0; + size_t end = 0; + + while ((end = str.find("\r\n", start)) != std::string::npos) { + std::string line = str.substr(start, end - start); + trim(line); + if (!line.empty()) lines.push_back(line); + start = end + 2; + } + + if (start < str.size()) { + std::string last = str.substr(start); + trim(last); + if (!last.empty()) lines.push_back(last); + } + + if (lines.empty()) return; + + // ---- DETECT WHICH COMMAND WAS USED ---- + std::string command = lines[0]; // first line (GetErr, GetState, etc.) + + + ESP_LOGI("uart_parser", "COMMAND: %s", command.c_str()); + // ---- OPTIONAL: print each line for debugging ---- + for (auto &line : lines) { + std::vector v(line.begin(), line.end()); + UARTDebug::log_string(direction, v); + //ESP_LOGI("uart_parser", "LINE: %s", line.c_str()); + } + + + + // ---- SPECIAL PARSING FOR GetErr ---- + if (command == "GetErr") { + // A GetErr response looks like: + // 0: GetErr + // 1: Error + // 2: + // 3: Alert + // 4: + // 5: USB state + // 6: NOT connected + + if (lines.size() < 7) return; + if (lines[1] != "Error" || lines[3] != "Alert") return; + + id(robotError).publish_state(lines[2].c_str()); + id(robotAlert).publish_state(lines[4].c_str()); + id(usbConnected).publish_state(lines[6].find("NOT connected") == std::string::npos); + + // If we can get erros/alerts, the system is ready for other commands: + if (!id(initial_ready)) { + id(initial_ready) = true; + id(initial_data_get).execute(); + } + } + else if (command == "GetCharger") { + // Format after the header: + // Label,Value + // FuelPercent,53 + // BatteryOverTemp,0 + // ... + + for (size_t i = 2; i < lines.size(); i++) { + std::string &line = lines[i]; + + size_t comma = line.find(','); + if (comma == std::string::npos) continue; + + std::string key = line.substr(0, comma); + std::string value = line.substr(comma + 1); + + // Trim them: + trim(key); + trim(value); + + ESP_LOGI("charger", "Parsed: key='%s' value='%s'", + key.c_str(), value.c_str()); + + // ---- MATCH KEYS AND PUBLISH ---- + if (key == "FuelPercent") id(chargerFuelPercent).publish_state(atof(value.c_str())); + else if (key == "BatteryOverTemp") id(chargerBatteryOverTemp).publish_state(value == "1"); + else if (key == "ChargingActive") id(chargerChargingActive).publish_state(value == "1"); + else if (key == "ChargingEnabled") id(chargerChargingEnabled).publish_state(value == "1"); + else if (key == "ConfidentOnFuel") id(chargerConfidentOnFuel).publish_state(value == "1"); + else if (key == "OnReservedFuel") id(chargerOnReservedFuel).publish_state(value == "1"); + else if (key == "EmptyFuel") id(chargerEmptyFuel).publish_state(value == "1"); + else if (key == "BatteryFailure") id(chargerBatteryFailure).publish_state(value == "1"); + else if (key == "ExtPwrPresent") id(chargerExtPwrPresent).publish_state(value == "1"); + else if (key == "ThermistorPresent") id(chargerThermistorPresent).publish_state(value == "1"); + else if (key == "BattTempCAvg") id(chargerBattTempCAvg).publish_state(atof(value.c_str())); + else if (key == "VBattV") id(chargerVBattV).publish_state(atof(value.c_str())); + else if (key == "VExtV") id(chargerVExtV).publish_state(atof(value.c_str())); + else if (key == "Charger_mAH") id(chargerCharger_mAH).publish_state(atof(value.c_str())); + else if (key == "Discharge_mAH") id(chargerDischarge_mAH).publish_state(atof(value.c_str())); + } + } + + else if (command == "GetWarranty") { + // Format after the header: + // Item,Value + // CumulativeCleaningTimeInSecs,001ce34f + // CumulativeBatteryCycles,01f7 + // ValidationCode,15648b6e + + for (size_t i = 2; i < lines.size(); i++) { + std::string &line = lines[i]; + size_t comma = line.find(','); + if (comma == std::string::npos) continue; + + std::string key = line.substr(0, comma); + std::string value = line.substr(comma + 1); + + // Trim whitespace + trim(key); + trim(value); + + if (key == "CumulativeBatteryCycles") { + uint32_t cycles = hex_to_uint32(value); + id(batteryCycles).publish_state(cycles); + } + } + } + + else if (command == "GetVersion") { + for (auto &line : lines) { + size_t comma = line.find(','); + if (comma == std::string::npos) continue; + + std::string key = line.substr(0, comma); + std::string value = line.substr(comma + 1); + trim(key); + trim(value); + + if (key == "MainBoard Serial Number") id(mainBoardSerial).publish_state(value.c_str()); + else if (key == "MainBoard Version") id(mainBoardVersion).publish_state(value.c_str()); + else if (key == "Serial Number") id(robotSerial).publish_state(value.c_str()); + else if (key == "Model") id(robotModel).publish_state(value.c_str()); + else if (key == "Time Local") id(timeLocal).publish_state(value.c_str()); + else if (key == "Time UTC") id(timeUTC).publish_state(value.c_str()); + } + } + // ------------------------------ TESTMODE ----------------------------------------- + else if (command == "TestMode") { + ESP_LOGI("testmode", "LINE1: %s", lines[1].c_str()); + //ESP_LOGI("testmode", "LINE1: %s", lines[1].c_str()); + if (lines[1].find(": On") != std::string::npos) id(testMode).publish_state(true); + else if (lines[1].find(": Off") != std::string::npos) id(testMode).publish_state(false); + } + + else if (command == "GetState") { + std::string &states_line = lines[1]; // "Current UI State is: ...\nCurrent Robot State is: ...\n\x1A" + + size_t sep = states_line.find('\n'); + if (sep == std::string::npos) return; // safety + + std::string ui_line = states_line.substr(0, sep); + std::string robot_line = states_line.substr(sep + 1); + + static const char* prefix_ui = "Current UI State is: "; + static const char* prefix_robot = "Current Robot State is: "; + + // Extract UI state + if (ui_line.find(prefix_ui) == 0) { + std::string ui_state = ui_line.substr(strlen(prefix_ui)); + trim(ui_state); + std::string old_state = id(uiState).state; + + bool old_spot = (old_state == "UIMGR_STATE_SPOTCLEANINGPAUSED" || + old_state == "UIMGR_STATE_SPOTCLEANINGRUNNING"); + + bool new_spot = (ui_state == "UIMGR_STATE_SPOTCLEANINGPAUSED" || + ui_state == "UIMGR_STATE_SPOTCLEANINGRUNNING"); + + bool old_house = (old_state == "UIMGR_STATE_HOUSECLEANINGPAUSED" || + old_state == "UIMGR_STATE_HOUSECLEANINGRUNNING"); + + bool new_house = (ui_state == "UIMGR_STATE_HOUSECLEANINGPAUSED" || + ui_state == "UIMGR_STATE_HOUSECLEANINGRUNNING"); + // Cleaning started + if (!old_spot && new_spot) { + // Mark start time + id(g_last_cleaning_start_time) = id(homeassistant_time).now().timestamp; + id(g_last_cleaning_type) = 1; // SPOT = 1 + + id(last_cleaning_time).publish_state(id(homeassistant_time).now().strftime("%Y-%m-%d %H:%M:%S")); + id(last_cleaning_type).publish_state("SPOT"); + } else if (!old_house && new_house) { + id(g_last_cleaning_start_time) = id(homeassistant_time).now().timestamp; + id(g_last_cleaning_type) = 0; // HOUSE = 0 + + id(last_cleaning_time).publish_state(id(homeassistant_time).now().strftime("%Y-%m-%d %H:%M:%S")); + id(last_cleaning_type).publish_state("HOUSE"); + } else { + // Cleaning stopped + bool old_cleaning = old_spot || old_house; + bool new_cleaning = new_spot || new_house; + + if (old_cleaning && !new_cleaning) id(cleaning_stopped).execute(); + } + + id(uiState).publish_state(ui_state.c_str()); + } + + // Extract Robot state + if (robot_line.find(prefix_robot) == 0) { + std::string robot_state = robot_line.substr(strlen(prefix_robot)); + trim(robot_state); + id(robotState).publish_state(robot_state.c_str()); + } + } + + + else if (command == "GetUserSettings") { + for (auto &line : lines) { + trim(line); + + // Skip lines that start with binary/invalid chars + if (!line.empty() && static_cast(line[0]) >= 0x80) continue; + + // ---- Key,Value lines ---- + size_t comma = line.find(','); + if (comma != std::string::npos) { + std::string key = line.substr(0, comma); + std::string value = line.substr(comma + 1); + trim(key); + trim(value); + + if (key == "Language") id(language).publish_state(value.c_str()); + else if (key == "ClickSounds") id(clickSounds).publish_state(value == "ON"); + else if (key == "LED") id(led).publish_state(value == "ON"); + else if (key == "Wall Enable") id(wallEnable).publish_state(value == "ON"); + else if (key == "Eco Mode") id(ecoMode).publish_state(value == "ON"); + else if (key == "IntenseClean") id(intenseClean).publish_state(value == "ON"); + else if (key == "WiFi") id(wifiOnOff).publish_state(value == "ON"); + else if (key == "Melody Sounds") id(melodySounds).publish_state(value == "ON"); + else if (key == "Warning Sounds") id(warningSounds).publish_state(value == "ON"); + else if (key == "Bin Full Detect") id(binFullDetect).publish_state(value == "ON"); + else if (key == "Filter Change Time (seconds)") id(filterChangeTime).publish_state(atof(value.c_str())); + else if (key == "Brush Change Time (seconds)") id(brushChangeTime).publish_state(atof(value.c_str())); + else if (key == "Dirt Bin Alert Reminder Interval (minutes)") id(dirtBinAlertReminder).publish_state(atof(value.c_str())); + } + // ---- Special lines without commas ---- + else if (line.find("Current Dirt Bin Runtime is:") == 0) { + std::string val = line.substr(strlen("Current Dirt Bin Runtime is:")); + trim(val); + id(currentDirtBinRuntime).publish_state(atof(val.c_str())); + } + else if (line.find("Number of Cleanings where Dust Bin was Full is:") == 0) { + std::string val = line.substr(strlen("Number of Cleanings where Dust Bin was Full is:")); + trim(val); + id(numberDustBinFull).publish_state(atof(val.c_str())); + } + else if (line.find("Schedule is Enabled") != std::string::npos) id(scheduleEnabled).publish_state(true); + else if (line.find("Schedule is Disabled") != std::string::npos) id(scheduleEnabled).publish_state(false); + } + } else if (command.find("ARCHES Board") != std::string::npos) id(uiState).publish_state("Starting..."); + + +sensor: + # --- GetCharger --- + - platform: template + id: chargerFuelPercent + name: "Fuel Percent" + unit_of_measurement: "%" + + - platform: template + id: chargerBattTempCAvg + name: "Battery Temp C Avg" + unit_of_measurement: "°C" + entity_category: "diagnostic" + + - platform: template + id: chargerVBattV + name: "Battery Voltage V" + unit_of_measurement: "V" + entity_category: "diagnostic" + + - platform: template + id: chargerVExtV + name: "External Voltage V" + unit_of_measurement: "V" + entity_category: "diagnostic" + + - platform: template + id: chargerCharger_mAH + name: "Charger mAh" + unit_of_measurement: "mAh" + entity_category: "diagnostic" + + - platform: template + id: chargerDischarge_mAH + name: "Discharge mAh" + unit_of_measurement: "mAh" + entity_category: "diagnostic" + + # --- GetUserSettings --- + - platform: template + name: "Filter Change Time" + id: filterChangeTime + unit_of_measurement: "s" + entity_category: "diagnostic" + + - platform: template + name: "Brush Change Time" + id: brushChangeTime + unit_of_measurement: "s" + entity_category: "diagnostic" + + - platform: template + name: "Dirt Bin Alert Reminder" + id: dirtBinAlertReminder + unit_of_measurement: "min" + entity_category: "diagnostic" + + - platform: template + name: "Current Dirt Bin Runtime" + id: currentDirtBinRuntime + unit_of_measurement: "s" + entity_category: "diagnostic" + + - platform: template + name: "Number of Full Dust Bin Cleanings" + id: numberDustBinFull + unit_of_measurement: "count" + entity_category: "diagnostic" + + # --- GetWarranty --- + - platform: template + name: "Battery Cycles" + id: batteryCycles + unit_of_measurement: "cycles" + entity_category: "diagnostic" + + # --- Last cleaning --- + - platform: template + name: "Last cleaning duration" + id: last_cleaning_duration + unit_of_measurement: "min" + + +binary_sensor: + # --- GetErr --- + - platform: template + id: usbConnected + name: "USB Connected" + entity_category: "diagnostic" + + # --- GetCharger --- + - platform: template + id: chargerBatteryOverTemp + name: "Battery Over Temp" + entity_category: "diagnostic" + + - platform: template + id: chargerChargingActive + name: "Charging Active" + entity_category: "diagnostic" + + - platform: template + id: chargerChargingEnabled + name: "Charging Enabled" + entity_category: "diagnostic" + + - platform: template + id: chargerConfidentOnFuel + name: "Confident On Fuel" + entity_category: "diagnostic" + + - platform: template + id: chargerOnReservedFuel + name: "On Reserved Fuel" + entity_category: "diagnostic" + + - platform: template + id: chargerEmptyFuel + name: "Empty Fuel" + entity_category: "diagnostic" + + - platform: template + id: chargerBatteryFailure + name: "Battery Failure" + entity_category: "diagnostic" + + - platform: template + id: chargerExtPwrPresent + name: "Ext Power Present" + entity_category: "diagnostic" + + - platform: template + id: chargerThermistorPresent + name: "Thermistor Present" + entity_category: "diagnostic" + +time: + - platform: homeassistant + id: homeassistant_time + +text_sensor: + # --- Last cleaning --- + - platform: template + name: "Last cleaning time" + id: last_cleaning_time + + - platform: template + name: "Last cleaning type" + id: last_cleaning_type + + # --- GetErr --- + - platform: template + id: robotError + name: "Robot Error" + + - platform: template + id: robotAlert + name: "Robot Alert" + + # --- GetVersion --- + - platform: template + id: mainBoardSerial + name: "MainBoard Serial Number" + entity_category: "diagnostic" + + - platform: template + id: mainBoardVersion + name: "MainBoard Version" + entity_category: "diagnostic" + + - platform: template + id: robotSerial + name: "Serial Number" + entity_category: "diagnostic" + on_value: + then: + - lambda: !lambda |- + // Get the mac address, abort if the serial is malformed + size_t comma_pos = x.find(','); + if (comma_pos == std::string::npos) { + ESP_LOGE("config", "Serial invalid (no comma): %s", x.c_str()); + abort(); + } + + std::string mac = x.substr(comma_pos + 1, 12); + + if (mac.length() != 12) { + ESP_LOGE("config", "Invalid MAC length (%d): %s", mac.length(), mac.c_str()); + abort(); + } + + // Calculate based on mac + static uint8_t t1[256]; + static uint8_t t2[256]; + static char result[128]; + const uint8_t byte_array[] = { 0x68, 0x36, 0x43, 0x58, 0x09, 0x09, 0x3A, 0x3C, 0x2A, 0x7B, 0x59 }; + + for (int i = 0; i < 256; i++) + t1[i] = i; + + int j = 0, i = 0; + for (int i = 0; i < 256; i++) { + j = (j + t1[i] + byte_array[i % 11]) & 0xFF; + uint8_t tmp = t1[i]; + t1[i] = t1[j]; + t1[j] = tmp; + } + + j = 0; + for (size_t k = 0; k < 12; k++) { + i = (i + 1) & 0xFF; + j = (j + t1[i]) & 0xFF; + + uint8_t tmp = t1[i]; + t1[i] = t1[j]; + t1[j] = tmp; + + t2[k] = t1[(t1[i] + t1[j]) & 0xFF]; + } + + size_t len = mac.length(); + + for (size_t j = 0; j < len; j++) { + snprintf(&result[j * 2], 3, "%02x", t2[j] ^ (uint8_t) mac[j]); + } + + result[len * 2] = result[len / 2]; + id(robot_skey) = std::string(result); + + + - platform: template + id: robotModel + name: "Model" + entity_category: "diagnostic" + + - platform: template + id: timeLocal + name: "Time Local" + entity_category: "diagnostic" + + - platform: template + id: timeUTC + name: "Time UTC" + entity_category: "diagnostic" + + # --- GetState --- + - platform: template + name: "UI State" + id: uiState + + - platform: template + name: "Robot State" + id: robotState + + # --- GetUserSettings --- + - platform: template + name: "Language" + id: language + entity_category: "diagnostic" + + +switch: + - platform: template + name: "Test Mode" + # optimistic: true + id: testMode + turn_on_action: + - uart.write: "TestMode On \n" + - delay: 500ms + - uart.write: "TestMode \n" + turn_off_action: + - uart.write: "TestMode Off \n" + - delay: 500ms + - uart.write: "TestMode \n" + entity_category: "diagnostic" + + - platform: template + name: "Play Extra Sounds" + optimistic: true + id: switch_play_extra_sounds + turn_on_action: + - lambda: 'id(g_play_extra_sounds) = true;' + turn_off_action: + - lambda: 'id(g_play_extra_sounds) = false;' + entity_category: "diagnostic" + + # --- SetUserSettings --- + - platform: template + name: "Click sounds" + optimistic: true + id: clickSounds + turn_on_action: + - uart.write: "SetUserSettings ButtonClick ON \n" + - script.execute: get_user_settings + turn_off_action: + - uart.write: "SetUserSettings ButtonClick OFF \n" + - script.execute: get_user_settings + entity_category: "diagnostic" + + - platform: template + name: "LED" + optimistic: true + id: led + turn_on_action: + - uart.write: "SetUserSettings StealthLED ON \n" + - script.execute: get_user_settings + turn_off_action: + - uart.write: "SetUserSettings StealthLED OFF \n" + - script.execute: get_user_settings + entity_category: "diagnostic" + + - platform: template + name: "Wall Enable" + optimistic: true + id: wallEnable + turn_on_action: + - uart.write: "SetUserSettings WallEnable ON \n" + - delay: 100ms + - uart.write: "GetUserSettings \n" + turn_off_action: + - uart.write: "SetUserSettings WallEnable OFF \n" + - delay: 100ms + - uart.write: "GetUserSettings \n" + entity_category: "diagnostic" + + - platform: template + name: "Eco Mode" + optimistic: true + id: ecoMode + turn_on_action: + - uart.write: "SetUserSettings EcoMode ON \n" + - script.execute: get_user_settings + turn_off_action: + - uart.write: "SetUserSettings EcoMode OFF \n" + - script.execute: get_user_settings + entity_category: "diagnostic" + + - platform: template + name: "IntenseClean" + optimistic: true + id: intenseClean + turn_on_action: + - uart.write: "SetUserSettings IntenseClean ON \n" + - script.execute: get_user_settings + turn_off_action: + - uart.write: "SetUserSettings IntenseClean OFF \n" + - script.execute: get_user_settings + entity_category: "diagnostic" + + - platform: template + name: "WiFi" + optimistic: true + id: wifiOnOff + turn_on_action: + - uart.write: "SetUserSettings WiFi ON \n" + - script.execute: get_user_settings + turn_off_action: + - uart.write: "SetUserSettings WiFi OFF \n" + - script.execute: get_user_settings + entity_category: "diagnostic" + + - platform: template + name: "Melody Sounds" + optimistic: true + id: melodySounds + turn_on_action: + - uart.write: "SetUserSettings Melodies ON \n" + - script.execute: get_user_settings + turn_off_action: + - uart.write: "SetUserSettings Melodies OFF \n" + - script.execute: get_user_settings + entity_category: "diagnostic" + + - platform: template + name: "Warning Sounds" + optimistic: true + id: warningSounds + turn_on_action: + - uart.write: "SetUserSettings Warnings ON \n" + - script.execute: get_user_settings + turn_off_action: + - uart.write: "SetUserSettings Warnings OFF \n" + - script.execute: get_user_settings + entity_category: "diagnostic" + + - platform: template + name: "Bin Full Detect" + optimistic: true + id: binFullDetect + turn_on_action: + - uart.write: "SetUserSettings BinFullDetect ON \n" + - script.execute: get_user_settings + turn_off_action: + - uart.write: "SetUserSettings BinFullDetect OFF \n" + - script.execute: get_user_settings + entity_category: "diagnostic" + + - platform: template + name: "Schedule" + optimistic: true + id: scheduleEnabled + turn_on_action: + - uart.write: "SetUserSettings Schedule ON \n" + - script.execute: get_user_settings + turn_off_action: + - uart.write: "SetUserSettings Schedule OFF \n" + - script.execute: get_user_settings + entity_category: "diagnostic" + + +number: + # --- Spot cleaning --- + - platform: template + id: spot_width + min_value: 100 + max_value: 400 + step: 1 + unit_of_measurement: cm + mode: SLIDER + name: Spot Clean Width + optimistic: true + + - platform: template + id: spot_height + min_value: 100 + max_value: 400 + step: 1 + unit_of_measurement: cm + mode: SLIDER + name: Spot Clean Height + optimistic: true + + +select: + - platform: template + name: "Navigation Mode" + id: navigationMode + optimistic: true + options: + - "Normal" + - "Gentle" + - "Deep" + - "Quick" + icon: "mdi:robot-vacuum" + on_value: + then: + - uart.write: !lambda |- + std::string cmd = std::string("SetNavigationMode ") + x + "\n"; + return std::vector(cmd.begin(), cmd.end()); + + +button: + - platform: template + name: "House Clean" + icon: mdi:home + on_press: + - script.execute: + id: play_extra_sounds + soundid: 1 + - script.execute: + id: send_event + event: "UIMGR_EVENT_SMARTAPP_START_HOUSE_CLEANING" + + - platform: template + name: "Spot Clean" + icon: mdi:target + on_press: + - script.execute: + id: play_extra_sounds + soundid: 1 + - script.execute: + id: send_event + event: "UIMGR_EVENT_SMARTAPP_START_SPOT_CLEANING" + + - platform: template + name: "Spot Clean (Height & Width)" + icon: mdi:target + on_press: + then: + - uart.write: !lambda |- + // Convert numbers to integers + int w = (int) id(spot_width).state; + int h = (int) id(spot_height).state; + + std::string cmd = "Clean Spot Width " + std::to_string(w) + " Height " + std::to_string(h) + "\n"; + id(play_extra_sounds)->execute(1); + return std::vector(cmd.begin(), cmd.end()); + + - platform: template + name: "Stop Cleaning" + icon: mdi:stop + on_press: + - script.execute: + id: send_event + event: "UIMGR_EVENT_SMARTAPP_STOP_CLEANING" + + - platform: template + name: "Pause Cleaning" + icon: mdi:pause + on_press: + - script.execute: + id: send_event + event: "UIMGR_EVENT_SMARTAPP_PAUSE_CLEANING" + + - platform: template + name: "Send to base" + icon: mdi:home + on_press: + - script.execute: + id: send_event + event: "UIMGR_EVENT_SMARTAPP_SEND_TO_BASE" + + - platform: template + name: "Resume Cleaning" + icon: mdi:play + on_press: + - script.execute: + id: send_event + event: "UIMGR_EVENT_SMARTAPP_RESUME_CLEANING" + + - platform: template + name: "Locate Robot" + icon: mdi:volume-high + on_press: + - uart.write: "PlaySound SoundId 20 \n" + + - platform: template + name: "Manual Drive Forward Up" + icon: mdi:arrow-up + on_press: + - script.execute: + id: send_event + event: "UIMGR_EVENT_SMARTAPP_DRIVE_MANUAL_FORWARD_UP" + + - platform: template + name: "Manual Drive Backwards Up" + icon: mdi:arrow-down + on_press: + - script.execute: + id: send_event + event: "UIMGR_EVENT_SMARTAPP_DRIVE_MANUAL_BACKWARDS_UP" + + - platform: template + name: "Manual Drive Turn Left Up" + icon: mdi:arrow-left + on_press: + - script.execute: + id: send_event + event: "UIMGR_EVENT_SMARTAPP_DRIVE_MANUAL_TURN_LEFT_UP" + + - platform: template + name: "Manual Drive Turn Right Up" + icon: mdi:arrow-right + on_press: + - script.execute: + id: send_event + event: "UIMGR_EVENT_SMARTAPP_DRIVE_MANUAL_TURN_RIGHT_UP" + + - platform: template + name: "Manual Drive Arc Left Up" + icon: mdi:rotate-left + on_press: + - script.execute: + id: send_event + event: "UIMGR_EVENT_SMARTAPP_DRIVE_MANUAL_ARC_LEFT_UP" + + - platform: template + name: "Manual Drive Arc Right Up" + icon: mdi:rotate-right + on_press: + - script.execute: + id: send_event + event: "UIMGR_EVENT_SMARTAPP_DRIVE_MANUAL_ARC_RIGHT_UP" + + - platform: template + name: "Manual Drive Forward Down" + icon: mdi:arrow-up-bold + on_press: + - script.execute: + id: send_event + event: "UIMGR_EVENT_SMARTAPP_DRIVE_MANUAL_FORWARD_DOWN" + + - platform: template + name: "Manual Drive Backwards Down" + icon: mdi:arrow-down-bold + on_press: + - script.execute: + id: send_event + event: "UIMGR_EVENT_SMARTAPP_DRIVE_MANUAL_BACKWARDS_DOWN" + + - platform: template + name: "Manual Drive Turn Left Down" + icon: mdi:arrow-left-bold + on_press: + - script.execute: + id: send_event + event: "UIMGR_EVENT_SMARTAPP_DRIVE_MANUAL_TURN_LEFT_DOWN" + + - platform: template + name: "Manual Drive Turn Right Down" + icon: mdi:arrow-right-bold + on_press: + - script.execute: + id: send_event + event: "UIMGR_EVENT_SMARTAPP_DRIVE_MANUAL_TURN_RIGHT_DOWN" + + - platform: template + name: "Manual Drive Arc Left Down" + icon: mdi:rotate-left + on_press: + - script.execute: + id: send_event + event: "UIMGR_EVENT_SMARTAPP_DRIVE_MANUAL_ARC_LEFT_DOWN" + + - platform: template + name: "Manual Drive Arc Right Down" + icon: mdi:rotate-right + on_press: + - script.execute: + id: send_event + event: "UIMGR_EVENT_SMARTAPP_DRIVE_MANUAL_ARC_RIGHT_DOWN" + + - platform: template + name: "Manual Drive Button Timeout" + icon: mdi:timer-off + on_press: + - script.execute: + id: send_event + event: "UIMGR_EVENT_SMARTAPP_DRIVE_MANUAL_BTN_TIMEOUT" + + - platform: template + name: "Start Manual Cleaning" + icon: mdi:play-circle + on_press: + - script.execute: + id: send_event + event: "UIMGR_EVENT_SMARTAPP_START_MANUAL_CLEANING" + + - platform: template + name: "Update status" + icon: mdi:refresh + on_press: + - script.execute: get_all_data + entity_category: "diagnostic" + + - platform: template + name: "Clear All Alerts & Errors" + icon: mdi:notification-clear-all + on_press: + - uart.write: "SetUIError clearall \n" + entity_category: "diagnostic" + + - platform: template + name: "Shutdown" + icon: mdi:power + on_press: + - script.execute: robotShutdown + entity_category: "diagnostic" + + - platform: template + name: "PowerCycle" + icon: mdi:restart + on_press: + - script.execute: powercycle + entity_category: "diagnostic" + + - platform: restart + name: Reboot ESP + diff --git a/secrets.yaml.example b/secrets.yaml.example new file mode 100644 index 0000000..f9561f1 --- /dev/null +++ b/secrets.yaml.example @@ -0,0 +1,21 @@ +# Copy this file to secrets.yaml and fill in your values +# The secrets.yaml file is gitignored for security + +# WiFi Configuration +wifi_ssid: "YourNetworkName" +wifi_password: "YourWiFiPassword" + +# API Configuration +# Generate at https://esphome.io/components/api/#api-key +api_encryption_key: "your-base64-encryption-key-here" + +# OTA Configuration +# Generate at https://bitwarden.com/password-generator/ +ota_password: "your-ota-password-here" + +# Optional: Syslog Server (if using syslog feature) +syslog_server: "X.X.X.X" + +# Optional: Captive Portal password (if using syslog feature) +captive_portal_password: "" +