Initial commit: ESP32-C5-EVB and ESP32-C6-EVB Arduino firmware + HA custom components
This commit is contained in:
+18
@@ -0,0 +1,18 @@
|
|||||||
|
# Sensitive credentials - never commit
|
||||||
|
secrets.h
|
||||||
|
|
||||||
|
# Arduino build artifacts
|
||||||
|
build/
|
||||||
|
*.bin
|
||||||
|
*.elf
|
||||||
|
*.map
|
||||||
|
firmware_build/
|
||||||
|
|
||||||
|
# Python cache
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
# NFC (PN532) Debugging Guide
|
||||||
|
|
||||||
|
## Current Status
|
||||||
|
- **Hardware**: GPIO26(TX) → PN532 RXD, GPIO25(RX) ← PN532 TXD (UEXT pins 3-4)
|
||||||
|
- **DIP Switches**: Both OFF (HSU mode) ✓
|
||||||
|
- **Power**: UEXT pin 1 = 3.3V (verified), Pin 2 = GND (verified)
|
||||||
|
- **Wiring**: Confirmed correct
|
||||||
|
- **Problem**: PN532 module not responding to auto-baud probe
|
||||||
|
|
||||||
|
## Why NFC Probe Fails
|
||||||
|
The firmware attempts to communicate with PN532 at various baud rates:
|
||||||
|
- 115200, 9600, 57600, 38400 baud
|
||||||
|
- Tries both GPIO26/GPIO25 and GPIO25/GPIO26 (swapped)
|
||||||
|
- At each combination: sends `getFirmwareVersion()` command
|
||||||
|
- **Issue**: No response from PN532 → returns 0x00
|
||||||
|
|
||||||
|
## Prerequisites for Debugging
|
||||||
|
### 1. Enable Serial Output
|
||||||
|
**Arduino IDE Settings (REQUIRED):**
|
||||||
|
```
|
||||||
|
Tools → USB CDC On Boot: ENABLED
|
||||||
|
Tools → Port: /dev/ttyACM0
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rebuild and flash the firmware:**
|
||||||
|
```bash
|
||||||
|
cd ~/arduino/olimex_ESP32-C5-EVB/esp32_arduino
|
||||||
|
arduino --verify --board esp32:esp32:esp32c5 esp32_arduino.ino
|
||||||
|
esptool.py --chip esp32c5 --port /dev/ttyACM0 --baud 921600 erase-flash
|
||||||
|
esptool.py --chip esp32c5 --port /dev/ttyACM0 --baud 921600 write-flash 0x0 build_output/esp32_arduino.ino.merged.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
**Open Serial Monitor:**
|
||||||
|
- Arduino IDE → Tools → Serial Monitor
|
||||||
|
- Baud rate: **115200**
|
||||||
|
- Wait for boot messages
|
||||||
|
|
||||||
|
## Diagnostic Steps
|
||||||
|
|
||||||
|
### Step 1: Verify Serial Output
|
||||||
|
Expected output during boot:
|
||||||
|
```
|
||||||
|
=================================
|
||||||
|
ESP32-C5 Home Assistant Device
|
||||||
|
Arduino Framework
|
||||||
|
=================================
|
||||||
|
...
|
||||||
|
--- NFC (PN532 HSU) Initialization with Debug ---
|
||||||
|
Hardware: GPIO26(TX)→PN532-RX, GPIO25(RX)←PN532-TX, DIP:HSU mode
|
||||||
|
[1/8] Baud=115200, RX=GPIO25, TX=GPIO26 ... ✗
|
||||||
|
[2/8] Baud=9600, RX=GPIO25, TX=GPIO26 ... ✗
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
**If you see:**
|
||||||
|
- ✓ All "✗" symbols → PN532 not responding at any baud rate
|
||||||
|
- Nothing after "Initialization" → Serial output not working (re-check USB CDC setting)
|
||||||
|
|
||||||
|
### Step 2: Physical Verification Checklist
|
||||||
|
```
|
||||||
|
□ PN532 powered: Measure 3.3V on VCC pin (should be 3.0-3.3V)
|
||||||
|
□ PN532 GND: Verify continuity to board GND with multimeter
|
||||||
|
□ UEXT Pin 1: Should be 3.3V (measure with multimeter)
|
||||||
|
□ UEXT Pin 2: Should be GND (continuity check)
|
||||||
|
□ UEXT Pin 3 (GPIO26): No continuity to Pin 4 (should be separate signals)
|
||||||
|
□ UEXT Pin 4 (GPIO25): No continuity to Pin 3 (should be separate signals)
|
||||||
|
□ PN532 Module: Try re-seating in connector or using different PN532 board
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Signal Analysis (Advanced)
|
||||||
|
If serial output shows all "✗" but hardware checks pass:
|
||||||
|
|
||||||
|
**Option A: Oscilloscope Check**
|
||||||
|
```
|
||||||
|
Connect oscilloscope probe to GPIO26 (TX line)
|
||||||
|
- Should see UART signal transitions during probe
|
||||||
|
- If flat line: ESP32 not transmitting
|
||||||
|
- If signal present: PN532 not responding
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option B: Loopback Test**
|
||||||
|
```
|
||||||
|
Connect GPIO26 (TX) directly to GPIO25 (RX) with a jumper wire
|
||||||
|
Rebuild with loopback test code (at end of this file)
|
||||||
|
If loopback works: GPIO pins OK, PN532 module is issue
|
||||||
|
If loopback fails: GPIO configuration issue
|
||||||
|
```
|
||||||
|
|
||||||
|
## Possible Issues & Solutions
|
||||||
|
|
||||||
|
### Issue 1: PN532 Module Defective
|
||||||
|
**Symptoms:** All baud rates fail, hardware verified correct
|
||||||
|
**Solution:**
|
||||||
|
- Try different PN532 board if available
|
||||||
|
- Test with I2C mode (different DIP setting) if supported
|
||||||
|
- Replace PN532 module
|
||||||
|
|
||||||
|
### Issue 2: UEXT Connector Loose
|
||||||
|
**Symptoms:** Intermittent detection or complete failure
|
||||||
|
**Solution:**
|
||||||
|
- Fully remove and re-seat PN532 in UEXT connector
|
||||||
|
- Ensure connector is fully inserted until it clicks
|
||||||
|
- Check for bent pins on UEXT connector
|
||||||
|
|
||||||
|
### Issue 3: Incorrect Pin Mapping
|
||||||
|
**Symptoms:** Detection works with swapped pins
|
||||||
|
**Solution:**
|
||||||
|
- Edit `esp32_arduino.ino`:
|
||||||
|
- Find: `#define NFC_TX_PIN 26` and `#define NFC_RX_PIN 25`
|
||||||
|
- If swapped RX/TX works: reverse these definitions
|
||||||
|
- Rebuild and test
|
||||||
|
|
||||||
|
### Issue 4: Baud Rate Mismatch
|
||||||
|
**Symptoms:** Serial output shows some attempts as "✓ FOUND!" but then fails to init
|
||||||
|
**Solution:**
|
||||||
|
- Firmware will re-init with confirmed baud rate
|
||||||
|
- If fails at SAMConfig() step: PN532 module may be in wrong mode
|
||||||
|
|
||||||
|
## Loopback Test Code
|
||||||
|
(If GPIO pins might be misconfigured):
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Add to setup() after WiFi init, BEFORE NFC init:
|
||||||
|
void setup() {
|
||||||
|
// ... existing code ...
|
||||||
|
|
||||||
|
// LOOPBACK TEST
|
||||||
|
Serial.println("--- GPIO Loopback Test ---");
|
||||||
|
Serial.println("Jumper GPIO26 to GPIO25 to test UART");
|
||||||
|
|
||||||
|
HardwareSerial testSerial(1); // UART1
|
||||||
|
testSerial.begin(115200, SERIAL_8N1, 25, 26); // RX=GPIO25, TX=GPIO26
|
||||||
|
|
||||||
|
delay(100);
|
||||||
|
testSerial.print("TEST_LOOPBACK");
|
||||||
|
delay(100);
|
||||||
|
|
||||||
|
String received = "";
|
||||||
|
while (testSerial.available()) {
|
||||||
|
received += (char)testSerial.read();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (received.indexOf("TEST") >= 0) {
|
||||||
|
Serial.println("✓ GPIO26/GPIO25 loopback OK");
|
||||||
|
} else {
|
||||||
|
Serial.println("✗ GPIO loopback FAILED - pin configuration issue");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
1. **Enable Serial Output**: Change USB CDC On Boot to ENABLED
|
||||||
|
2. **Rebuild & Flash**: Use updated Arduino IDE settings
|
||||||
|
3. **Monitor Serial**: Watch boot output during NFC probe
|
||||||
|
4. **Share Output**: Post the serial output here for analysis
|
||||||
|
|
||||||
|
**Expected Timeline:**
|
||||||
|
- Checking serial output: ~5 minutes
|
||||||
|
- Physical verification: ~10 minutes
|
||||||
|
- Analyzing results: ~5 minutes
|
||||||
|
- **Total**: ~20 minutes to identify root cause
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
# Olimex ESP32-C5-EVB — Home Assistant Integration
|
||||||
|
|
||||||
|
This repository contains two components:
|
||||||
|
|
||||||
|
## `custom_components/olimex_esp32_c5`
|
||||||
|
Home Assistant custom integration for the **Olimex ESP32-C5-EVB** board.
|
||||||
|
|
||||||
|
- 2 relay switches (250V / 10A, controlled via HTTP POST to the board)
|
||||||
|
- 2 opto-isolated digital inputs (110VAC-240VAC, state pushed from board to HA via webhook)
|
||||||
|
- NFC/PN532 card reader for access control (optional, via UEXT connector)
|
||||||
|
- No polling — fully event-driven for inputs, command-driven for relays
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
Copy `custom_components/olimex_esp32_c5` into your Home Assistant `config/custom_components/` directory and restart HA.
|
||||||
|
|
||||||
|
## `esp32_arduino`
|
||||||
|
Arduino sketch for the ESP32-C5-EVB board.
|
||||||
|
|
||||||
|
- Hosts a REST API on port 80
|
||||||
|
- Registers a callback URL with HA on startup
|
||||||
|
- POSTs input state changes to HA webhook in real time
|
||||||
|
- Optional NFC card reader integration for relay trigger automation
|
||||||
|
|
||||||
|
### Arduino IDE Settings
|
||||||
|
| Setting | Value |
|
||||||
|
|---|---|
|
||||||
|
| Board | ESP32C5 Dev Module |
|
||||||
|
| Flash Size | 4MB |
|
||||||
|
| USB CDC On Boot | Enabled |
|
||||||
|
|
||||||
|
See [`esp32_arduino/DEPLOYMENT_GUIDE.md`](esp32_arduino/DEPLOYMENT_GUIDE.md) for full flashing instructions.
|
||||||
|
|
||||||
|
## Key Differences from C6-EVB
|
||||||
|
|
||||||
|
| Feature | C6-EVB | C5-EVB |
|
||||||
|
|---------|--------|--------|
|
||||||
|
| **Relays** | 4 | 2 (250V / 10A) |
|
||||||
|
| **Inputs** | 4 | 2 (opto-isolated 110VAC-240VAC) |
|
||||||
|
| **Relay 1** | GPIO 10 | GPIO 24 |
|
||||||
|
| **Relay 2** | GPIO 11 | GPIO 23 |
|
||||||
|
| **Input 1** | GPIO 1 | GPIO 4 |
|
||||||
|
| **Input 2** | GPIO 2 | GPIO 5 |
|
||||||
|
| **LED** | GPIO 8 | GPIO 27 |
|
||||||
|
| **Button** | GPIO 9 | GPIO 28 |
|
||||||
|
| **NFC UART** | GPIO 4/5 (UEXT1) | GPIO 26/25 (UEXT) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Hardware Overview
|
||||||
|
|
||||||
|
### Pin Configuration
|
||||||
|
|
||||||
|
**Relays** (250V / 10A AC/DC)
|
||||||
|
- Relay 1: GPIO24
|
||||||
|
- Relay 2: GPIO23
|
||||||
|
|
||||||
|
**Opto-Isolated Inputs** (110VAC-240VAC)
|
||||||
|
- Opto Input 1: GPIO4
|
||||||
|
- Opto Input 2: GPIO5
|
||||||
|
|
||||||
|
**LED & Button**
|
||||||
|
- User LED: GPIO27 (active LOW)
|
||||||
|
- User Button: GPIO28 (active LOW, internal pull-up)
|
||||||
|
|
||||||
|
**UEXT Connector** (for NFC reader / additional peripherals)
|
||||||
|
- I2C: SDA=GPIO9, SCL=GPIO8
|
||||||
|
- UART: TX=GPIO26, RX=GPIO25
|
||||||
|
- SPI: CLK=GPIO2, MOSI=GPIO7, MISO=GPIO6
|
||||||
|
|
||||||
|
### Power & Connectivity
|
||||||
|
- USB-C power input
|
||||||
|
- WiFi 6 (802.11ax) dual-band (2.4 & 5 GHz)
|
||||||
|
- Bluetooth 5.0 LE
|
||||||
|
- Zigbee & Thread capable
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### Status & Control
|
||||||
|
- `GET /api/status` — Returns all relay and input states (JSON)
|
||||||
|
- `POST /relay/on?relay=1-2` — Turn relay on
|
||||||
|
- `POST /relay/off?relay=1-2` — Turn relay off
|
||||||
|
- `GET /relay/status?relay=1-2` — Get relay state
|
||||||
|
|
||||||
|
### Inputs
|
||||||
|
- `GET /input/status?input=1-2` — Get input state
|
||||||
|
|
||||||
|
### LED
|
||||||
|
- `POST /led/on` — Turn LED on
|
||||||
|
- `POST /led/off` — Turn LED off
|
||||||
|
|
||||||
|
### Home Assistant Integration
|
||||||
|
- `POST /register?callback_url=...` — Register HA webhook (no auth required for bootstrap)
|
||||||
|
|
||||||
|
### NFC Access Control
|
||||||
|
- `GET /nfc/status` — Get NFC module status
|
||||||
|
- `GET /nfc/config` — Get NFC access control settings
|
||||||
|
- `POST /nfc/config?auth_uid=...&relay=1-2&pulse_ms=5000` — Set NFC access config
|
||||||
|
- `POST /nfc/enable?state=0|1` — Enable/disable NFC module
|
||||||
|
|
||||||
|
### Debug
|
||||||
|
- `GET /debug` — System info (requires basic auth)
|
||||||
|
|
||||||
|
All endpoints except `/register` require **Basic HTTP Authentication** (default: admin/admin) or HMAC-SHA256 request signing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Setup Instructions
|
||||||
|
|
||||||
|
1. **Flash the firmware**
|
||||||
|
- See [DEPLOYMENT_GUIDE.md](esp32_arduino/DEPLOYMENT_GUIDE.md)
|
||||||
|
|
||||||
|
2. **Configure WiFi**
|
||||||
|
- Edit WiFi credentials in `esp32_arduino.ino` (lines ~58-59)
|
||||||
|
- Set a static IP (currently 192.168.0.240)
|
||||||
|
- Flash the board
|
||||||
|
|
||||||
|
3. **Add to Home Assistant**
|
||||||
|
- Copy `custom_components/olimex_esp32_c5/` to `~/.homeassistant/custom_components/`
|
||||||
|
- Restart Home Assistant
|
||||||
|
- Settings → Devices & Services → Create Integration → Olimex ESP32-C5-EVB
|
||||||
|
- Enter board IP, port (80), and Home Assistant callback IP
|
||||||
|
|
||||||
|
4. **Optional: NFC Card Reader Setup**
|
||||||
|
- Attach PN532 breakout to UEXT connector (TX=GPIO26, RX=GPIO25)
|
||||||
|
- Open board web UI (`http://<ip>/`)
|
||||||
|
- Enable NFC module
|
||||||
|
- Scan a card to authorize it
|
||||||
|
- Configure trigger relay and timeout
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Home Assistant Integration
|
||||||
|
|
||||||
|
Once configured, entities appear automatically:
|
||||||
|
|
||||||
|
**Switches**
|
||||||
|
- `switch.olimex_esp32_c5_relay_1`
|
||||||
|
- `switch.olimex_esp32_c5_relay_2`
|
||||||
|
|
||||||
|
**Binary Sensors**
|
||||||
|
- `binary_sensor.olimex_esp32_c5_input_1` (opto-isolated input)
|
||||||
|
- `binary_sensor.olimex_esp32_c5_input_2` (opto-isolated input)
|
||||||
|
- `binary_sensor.olimex_esp32_c5_nfc_card` (NFC card present, if enabled)
|
||||||
|
|
||||||
|
**Automations**
|
||||||
|
Create automations to trigger relays on input events, NFC cards, time schedules, etc.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Board not reachable
|
||||||
|
1. Check WiFi SSID and password in code
|
||||||
|
2. Verify board IP with serial console output
|
||||||
|
3. Check firewall allows port 80
|
||||||
|
|
||||||
|
### Inputs not triggering
|
||||||
|
1. Verify wiring to opto-isolated inputs (GPIO4, GPIO5)
|
||||||
|
2. Use `/debug` endpoint to check input state
|
||||||
|
3. Ensure Home Assistant callback URL is registered
|
||||||
|
|
||||||
|
### NFC not detecting cards
|
||||||
|
1. Check PN532 wiring (TX=GPIO26, RX=GPIO25)
|
||||||
|
2. Set PN532 DIP switches to HSU mode (both = 0)
|
||||||
|
3. Check serial output for PN532 detection message
|
||||||
|
4. Try different baud rates (115200, 9600, 57600, 38400)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security Notes
|
||||||
|
|
||||||
|
- Default credentials: admin / admin
|
||||||
|
- Change WEB_PASSWORD in `secrets.h` before deployment
|
||||||
|
- Enable API_SECRET for HMAC-SHA256 request signing (optional)
|
||||||
|
- Only expose board on trusted local networks
|
||||||
|
- NFC cards trigger relays when authenticated card is present
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## License & Attribution
|
||||||
|
|
||||||
|
- Hardware: [Olimex](https://www.olimex.com/Products/IoT/ESP32-C5/ESP32-C5-EVB/open-source-hardware)
|
||||||
|
- Firmware: Arduino & ESP-IDF
|
||||||
|
- Integration: Home Assistant Custom Component
|
||||||
@@ -0,0 +1,235 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Olimex ESP32-C5-EVB — Remote Board Verification Script
|
||||||
|
=========================================================
|
||||||
|
Queries the board's REST API and verifies every subsystem.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 board_verify.py # uses default IP 192.168.0.240
|
||||||
|
python3 board_verify.py 192.168.0.200 # custom IP
|
||||||
|
python3 board_verify.py --json # machine-readable output
|
||||||
|
|
||||||
|
Requirements: pip install requests (already in location_managemet requirements)
|
||||||
|
|
||||||
|
What it tests:
|
||||||
|
1. Board reachability (GET /api/status)
|
||||||
|
2. Both relays (POST /relay/on, GET /relay/status, POST /relay/off)
|
||||||
|
3. Both digital inputs (GET /input/status)
|
||||||
|
4. LED (POST /led/on + /led/off)
|
||||||
|
5. NFC reader (GET /nfc/status)
|
||||||
|
6. NFC config API (GET /nfc/config)
|
||||||
|
|
||||||
|
NOTE: Relay tests cycle each relay ON→verify→OFF→verify.
|
||||||
|
You should hear/see the relay click during the test.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import argparse
|
||||||
|
import requests
|
||||||
|
|
||||||
|
TIMEOUT = 5 # seconds per HTTP request
|
||||||
|
RELAY_DLY = 0.4 # seconds to wait between relay on/status/off
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Result tracking
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
def record(name: str, ok: bool, detail: str = "") -> bool:
|
||||||
|
results.append({"name": name, "pass": ok, "detail": detail})
|
||||||
|
icon = "\033[32m[PASS]\033[0m" if ok else "\033[31m[FAIL]\033[0m"
|
||||||
|
print(f" {icon} {name}" + (f" — {detail}" if detail else ""))
|
||||||
|
return ok
|
||||||
|
|
||||||
|
|
||||||
|
def _get(url: str):
|
||||||
|
try:
|
||||||
|
r = requests.get(url, timeout=TIMEOUT)
|
||||||
|
return r.status_code, r.text, None
|
||||||
|
except requests.RequestException as e:
|
||||||
|
return None, None, str(e)
|
||||||
|
|
||||||
|
|
||||||
|
def _post(url: str, json_data=None):
|
||||||
|
try:
|
||||||
|
r = requests.post(url, json=json_data, timeout=TIMEOUT)
|
||||||
|
return r.status_code, r.text, None
|
||||||
|
except requests.RequestException as e:
|
||||||
|
return None, None, str(e)
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Main verification
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def verify_board(ip: str) -> int:
|
||||||
|
"""Test all board subsystems. Returns count of failures."""
|
||||||
|
base = f"http://{ip}"
|
||||||
|
failures = 0
|
||||||
|
|
||||||
|
print(f"\n{'='*70}")
|
||||||
|
print(f"Olimex ESP32-C5-EVB Verification")
|
||||||
|
print(f"{'='*70}")
|
||||||
|
print(f"Target IP: {ip}\n")
|
||||||
|
|
||||||
|
# ─ 1. Board Reachability ──────────────────────────────────────────────────
|
||||||
|
print("1. Board Status")
|
||||||
|
status_code, resp_text, err = _get(f"{base}/api/status")
|
||||||
|
if err:
|
||||||
|
record("Board reachable", False, err)
|
||||||
|
return 1 # Can't proceed without board connectivity
|
||||||
|
if not (status_code and status_code == 200):
|
||||||
|
record("Board reachable", False, f"HTTP {status_code}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
try:
|
||||||
|
status_json = json.loads(resp_text)
|
||||||
|
record("Board reachable", True, f"HTTP 200")
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
record("Board reachable (partial)", False, "Response not JSON")
|
||||||
|
status_json = {}
|
||||||
|
|
||||||
|
# ─ 2. Relays ──────────────────────────────────────────────────────────────
|
||||||
|
print("\n2. Relays (2x 250V/10A)")
|
||||||
|
for relay_num in [1, 2]:
|
||||||
|
relay_name = f"Relay {relay_num}"
|
||||||
|
|
||||||
|
# Turn ON
|
||||||
|
_, _, err = _post(f"{base}/relay/on?relay={relay_num}")
|
||||||
|
if err:
|
||||||
|
record(f"{relay_name} ON", False, err)
|
||||||
|
failures += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
time.sleep(RELAY_DLY)
|
||||||
|
status_code, resp_text, err = _get(f"{base}/relay/status?relay={relay_num}")
|
||||||
|
if err or status_code != 200:
|
||||||
|
record(f"{relay_name} ON verify", False, err or f"HTTP {status_code}")
|
||||||
|
failures += 1
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
r = json.loads(resp_text)
|
||||||
|
if r.get("state") == True:
|
||||||
|
record(f"{relay_name} ON", True)
|
||||||
|
else:
|
||||||
|
record(f"{relay_name} ON verify", False, "State mismatch")
|
||||||
|
failures += 1
|
||||||
|
except:
|
||||||
|
record(f"{relay_name} ON verify", False, "Invalid response")
|
||||||
|
failures += 1
|
||||||
|
|
||||||
|
time.sleep(RELAY_DLY)
|
||||||
|
|
||||||
|
# Turn OFF
|
||||||
|
_, _, err = _post(f"{base}/relay/off?relay={relay_num}")
|
||||||
|
if err:
|
||||||
|
record(f"{relay_name} OFF", False, err)
|
||||||
|
failures += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
time.sleep(RELAY_DLY)
|
||||||
|
status_code, resp_text, err = _get(f"{base}/relay/status?relay={relay_num}")
|
||||||
|
if err or status_code != 200:
|
||||||
|
record(f"{relay_name} OFF verify", False, err or f"HTTP {status_code}")
|
||||||
|
failures += 1
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
r = json.loads(resp_text)
|
||||||
|
if r.get("state") == False:
|
||||||
|
record(f"{relay_name} OFF", True)
|
||||||
|
else:
|
||||||
|
record(f"{relay_name} OFF verify", False, "State mismatch")
|
||||||
|
failures += 1
|
||||||
|
except:
|
||||||
|
record(f"{relay_name} OFF verify", False, "Invalid response")
|
||||||
|
failures += 1
|
||||||
|
|
||||||
|
# ─ 3. Digital Inputs ──────────────────────────────────────────────────────
|
||||||
|
print("\n3. Opto-Isolated Inputs (110VAC-240VAC)")
|
||||||
|
for input_num in [1, 2]:
|
||||||
|
status_code, resp_text, err = _get(f"{base}/input/status?input={input_num}")
|
||||||
|
if err or status_code != 200:
|
||||||
|
record(f"Input {input_num}", False, err or f"HTTP {status_code}")
|
||||||
|
failures += 1
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
r = json.loads(resp_text)
|
||||||
|
state = "HIGH" if r.get("state") else "LOW"
|
||||||
|
record(f"Input {input_num}", True, f"State: {state}")
|
||||||
|
except:
|
||||||
|
record(f"Input {input_num}", False, "Invalid response")
|
||||||
|
failures += 1
|
||||||
|
|
||||||
|
# ─ 4. LED ─────────────────────────────────────────────────────────────────
|
||||||
|
print("\n4. LED Control")
|
||||||
|
_, _, err = _post(f"{base}/led/on")
|
||||||
|
if err:
|
||||||
|
record("LED ON", False, err)
|
||||||
|
failures += 1
|
||||||
|
else:
|
||||||
|
record("LED ON", True)
|
||||||
|
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
_, _, err = _post(f"{base}/led/off")
|
||||||
|
if err:
|
||||||
|
record("LED OFF", False, err)
|
||||||
|
failures += 1
|
||||||
|
else:
|
||||||
|
record("LED OFF", True)
|
||||||
|
|
||||||
|
# ─ 5. NFC Status ──────────────────────────────────────────────────────────
|
||||||
|
print("\n5. NFC Module (PN532 via UEXT, optional)")
|
||||||
|
status_code, resp_text, err = _get(f"{base}/nfc/status")
|
||||||
|
if err or status_code != 200:
|
||||||
|
record("NFC Status API", False, err or f"HTTP {status_code}")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
nfc_status = json.loads(resp_text)
|
||||||
|
init = nfc_status.get("initialized", False)
|
||||||
|
record("NFC Status API", True, f"Init: {init}")
|
||||||
|
except:
|
||||||
|
record("NFC Status API", False, "Invalid response")
|
||||||
|
|
||||||
|
# ─ 6. NFC Config ──────────────────────────────────────────────────────────
|
||||||
|
status_code, resp_text, err = _get(f"{base}/nfc/config")
|
||||||
|
if err or status_code != 200:
|
||||||
|
record("NFC Config API", False, err or f"HTTP {status_code}")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
config = json.loads(resp_text)
|
||||||
|
record("NFC Config API", True, f"Relay: {config.get('relay_num')}, Pulse: {config.get('pulse_ms')}ms")
|
||||||
|
except:
|
||||||
|
record("NFC Config API", False, "Invalid response")
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
|
# Summary
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
|
print(f"\n{'='*70}")
|
||||||
|
passed = len([r for r in results if r["pass"]])
|
||||||
|
total = len(results)
|
||||||
|
print(f"Result: {passed}/{total} tests passed")
|
||||||
|
if failures == 0:
|
||||||
|
print("✓ All systems functional!")
|
||||||
|
else:
|
||||||
|
print(f"✗ {failures} failures detected")
|
||||||
|
print(f"{'='*70}\n")
|
||||||
|
|
||||||
|
return failures
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("ip", nargs="?", default="192.168.0.240", help="Board IP (default: 192.168.0.240)")
|
||||||
|
parser.add_argument("--json", action="store_true", help="Output results as JSON")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
failures = verify_board(args.ip)
|
||||||
|
|
||||||
|
if args.json:
|
||||||
|
print(json.dumps(results, indent=2))
|
||||||
|
|
||||||
|
sys.exit(min(failures, 1))
|
||||||
Executable
+133
@@ -0,0 +1,133 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Build and Deploy Script for ESP32-C5-EVB
|
||||||
|
# Usage: ./build_and_deploy.sh [--flash-only]
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
SKETCH_DIR="$SCRIPT_DIR/esp32_arduino"
|
||||||
|
SKETCH_FILE="$SKETCH_DIR/esp32_arduino.ino"
|
||||||
|
DEVICE_PORT="${DEVICE_PORT:-/dev/ttyACM0}"
|
||||||
|
DEVICE_BAUD="${DEVICE_BAUD:-921600}"
|
||||||
|
DEVICE_IP="${DEVICE_IP:-192.168.0.241}"
|
||||||
|
|
||||||
|
# ─ Colors ─────────────────────────────────────────────────────────────────────
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# ─ Functions ──────────────────────────────────────────────────────────────────
|
||||||
|
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
||||||
|
log_success() { echo -e "${GREEN}[✓]${NC} $1"; }
|
||||||
|
log_warn() { echo -e "${YELLOW}[⚠]${NC} $1"; }
|
||||||
|
log_error() { echo -e "${RED}[✗]${NC} $1"; }
|
||||||
|
|
||||||
|
# ─ Check Prerequisites ────────────────────────────────────────────────────────
|
||||||
|
log_info "Checking prerequisites..."
|
||||||
|
|
||||||
|
if [ ! -f "$SKETCH_FILE" ]; then
|
||||||
|
log_error "Sketch not found: $SKETCH_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v esptool.py &> /dev/null; then
|
||||||
|
log_error "esptool.py not found. Install: pip install esptool"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -e "$DEVICE_PORT" ]; then
|
||||||
|
log_error "Device port not found: $DEVICE_PORT"
|
||||||
|
log_info "Available ports:"
|
||||||
|
ls -la /dev/ttyUSB* /dev/ttyACM* 2>/dev/null || echo " (none found)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_success "Prerequisites OK"
|
||||||
|
|
||||||
|
# ─ Skip compilation if --flash-only ───────────────────────────────────────────
|
||||||
|
if [ "$1" == "--flash-only" ]; then
|
||||||
|
log_warn "Skipping compilation (--flash-only)"
|
||||||
|
LATEST_BUILD=$(find /tmp -name "arduino_build_*" -type d 2>/dev/null | sort -V | tail -1)
|
||||||
|
else
|
||||||
|
# ─ Build ──────────────────────────────────────────────────────────────────
|
||||||
|
log_info "Compiling sketch (this may take 30-60 seconds)..."
|
||||||
|
log_warn "Please click 'Sketch → Verify' in Arduino IDE or use: arduino --verify --board esp32:esp32:esp32c5 '$SKETCH_FILE'"
|
||||||
|
log_info ""
|
||||||
|
log_info "Waiting for build to complete..."
|
||||||
|
|
||||||
|
# Find the latest build directory created
|
||||||
|
BUILD_BEFORE=$(find /tmp -name "arduino_build_*" -type d 2>/dev/null | sort -V | tail -1)
|
||||||
|
START_TIME=$(date +%s)
|
||||||
|
|
||||||
|
# Wait for new build or changes to existing build (max 120 seconds)
|
||||||
|
while true; do
|
||||||
|
LATEST_BUILD=$(find /tmp -name "arduino_build_*" -type d 2>/dev/null | sort -V | tail -1)
|
||||||
|
|
||||||
|
if [ -n "$LATEST_BUILD" ] && [ -f "$LATEST_BUILD/esp32_arduino.ino.merged.bin" ]; then
|
||||||
|
MODIFIED=$(stat -f%m "$LATEST_BUILD/esp32_arduino.ino.merged.bin" 2>/dev/null || stat -c%Y "$LATEST_BUILD/esp32_arduino.ino.merged.bin")
|
||||||
|
CURRENT_TIME=$(date +%s)
|
||||||
|
|
||||||
|
if [ $((CURRENT_TIME - MODIFIED)) -lt 10 ]; then
|
||||||
|
log_success "Build found and appears recent"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
CURRENT_TIME=$(date +%s)
|
||||||
|
if [ $((CURRENT_TIME - START_TIME)) -gt 120 ]; then
|
||||||
|
log_error "Timeout waiting for build. Please compile in Arduino IDE manually."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ─ Verify binary exists ───────────────────────────────────────────────────────
|
||||||
|
if [ -z "$LATEST_BUILD" ] || [ ! -f "$LATEST_BUILD/esp32_arduino.ino.merged.bin" ]; then
|
||||||
|
log_error "Compiled binary not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
BINARY_PATH="$LATEST_BUILD/esp32_arduino.ino.merged.bin"
|
||||||
|
BINARY_SIZE=$(ls -lh "$BINARY_PATH" | awk '{print $5}')
|
||||||
|
log_success "Binary found: $BINARY_PATH ($BINARY_SIZE)"
|
||||||
|
|
||||||
|
# ─ Flash ──────────────────────────────────────────────────────────────────────
|
||||||
|
log_info "Erasing flash on $DEVICE_PORT..."
|
||||||
|
esptool.py --chip esp32c5 --port "$DEVICE_PORT" --baud "$DEVICE_BAUD" erase-flash
|
||||||
|
|
||||||
|
log_info "Flashing firmware..."
|
||||||
|
cd "$LATEST_BUILD"
|
||||||
|
esptool.py --chip esp32c5 --port "$DEVICE_PORT" --baud "$DEVICE_BAUD" write-flash 0x0 esp32_arduino.ino.merged.bin
|
||||||
|
|
||||||
|
log_success "Firmware flashed successfully!"
|
||||||
|
|
||||||
|
# ─ Verify ─────────────────────────────────────────────────────────────────────
|
||||||
|
log_info "Waiting for device to boot (10 seconds)..."
|
||||||
|
sleep 10
|
||||||
|
|
||||||
|
log_info "Testing connectivity to $DEVICE_IP..."
|
||||||
|
if curl -s -f -u Ske087:Matei@123 "http://$DEVICE_IP/relay/status?relay=1" &> /dev/null; then
|
||||||
|
log_success "Device is online and responding!"
|
||||||
|
|
||||||
|
# Test all endpoints
|
||||||
|
log_info "Testing API endpoints..."
|
||||||
|
echo -n " Relay 1: "
|
||||||
|
curl -s -u Ske087:Matei@123 "http://$DEVICE_IP/relay/status?relay=1"
|
||||||
|
echo -n " Relay 2: "
|
||||||
|
curl -s -u Ske087:Matei@123 "http://$DEVICE_IP/relay/status?relay=2"
|
||||||
|
echo -n " Input 1: "
|
||||||
|
curl -s -u Ske087:Matei@123 "http://$DEVICE_IP/input/status?input=1"
|
||||||
|
echo -n " Input 2: "
|
||||||
|
curl -s -u Ske087:Matei@123 "http://$DEVICE_IP/input/status?input=2"
|
||||||
|
|
||||||
|
log_success "All tests passed!"
|
||||||
|
else
|
||||||
|
log_warn "Device not yet responding. This is normal if WiFi connection is in progress."
|
||||||
|
log_info "Check with: curl -u Ske087:Matei@123 http://$DEVICE_IP/relay/status?relay=1"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_success "Deployment complete!"
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
"""Olimex ESP32-C5-EVB Integration for Home Assistant."""
|
||||||
|
import logging
|
||||||
|
import aiohttp
|
||||||
|
from homeassistant.config_entries import ConfigEntry, SOURCE_IMPORT
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.const import CONF_HOST, CONF_PORT, Platform
|
||||||
|
from homeassistant.components.webhook import (
|
||||||
|
async_register as webhook_register,
|
||||||
|
async_unregister as webhook_unregister,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .const import DOMAIN, CONF_CALLBACK_IP, DEFAULT_CALLBACK_IP
|
||||||
|
from .webhook import handle_input_event
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SWITCH]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup(hass: HomeAssistant, config: dict) -> bool:
|
||||||
|
"""Set up the Olimex ESP32-C5-EVB component."""
|
||||||
|
hass.data.setdefault(DOMAIN, {})
|
||||||
|
|
||||||
|
# Handle YAML configuration
|
||||||
|
if DOMAIN in config:
|
||||||
|
for device_config in config[DOMAIN]:
|
||||||
|
hass.async_create_task(
|
||||||
|
hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_IMPORT},
|
||||||
|
data=device_config,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
_LOGGER.debug("Olimex ESP32-C5-EVB integration initialized")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Set up Olimex ESP32-C5-EVB from a config entry."""
|
||||||
|
_LOGGER.info("Setting up Olimex ESP32-C5-EVB entry: %s", entry.entry_id)
|
||||||
|
|
||||||
|
host = entry.data.get(CONF_HOST, "192.168.0.240")
|
||||||
|
port = entry.data.get(CONF_PORT, 80)
|
||||||
|
callback_ip = entry.data.get(CONF_CALLBACK_IP, DEFAULT_CALLBACK_IP)
|
||||||
|
|
||||||
|
hass.data.setdefault(DOMAIN, {})
|
||||||
|
hass.data[DOMAIN][entry.entry_id] = {
|
||||||
|
"host": host,
|
||||||
|
"port": port,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Tell the board where to POST input events
|
||||||
|
try:
|
||||||
|
callback_url = f"http://{callback_ip}:8123/api/webhook/{entry.entry_id}"
|
||||||
|
register_url = f"http://{host}:{port}/register?callback_url={callback_url}"
|
||||||
|
_LOGGER.info("Registering webhook with board at %s:%d (callback %s)", host, port, callback_url)
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.post(register_url, timeout=aiohttp.ClientTimeout(total=5)) as response:
|
||||||
|
if response.status == 200:
|
||||||
|
_LOGGER.info("Board webhook registered successfully")
|
||||||
|
else:
|
||||||
|
_LOGGER.warning("Board registration returned status %d", response.status)
|
||||||
|
except Exception as err:
|
||||||
|
_LOGGER.warning("Failed to register webhook with board: %s", err)
|
||||||
|
|
||||||
|
# Register HA webhook handler to receive input events from the board
|
||||||
|
# Unregister first in case a previous failed setup left it registered
|
||||||
|
try:
|
||||||
|
webhook_unregister(hass, entry.entry_id)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
webhook_register(
|
||||||
|
hass,
|
||||||
|
DOMAIN,
|
||||||
|
"Olimex Input Event",
|
||||||
|
entry.entry_id,
|
||||||
|
handle_input_event,
|
||||||
|
)
|
||||||
|
_LOGGER.info("HA webhook handler registered for entry %s", entry.entry_id)
|
||||||
|
|
||||||
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
_LOGGER.info("Olimex ESP32-C5-EVB configured for %s:%d", host, port)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||||
|
"""Clean up when integration is fully removed (called after unload)."""
|
||||||
|
_LOGGER.info("Removing Olimex ESP32-C5-EVB entry: %s", entry.entry_id)
|
||||||
|
# Remove any leftover domain data bucket if it's now empty
|
||||||
|
if DOMAIN in hass.data and not hass.data[DOMAIN]:
|
||||||
|
hass.data.pop(DOMAIN)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Unload a config entry."""
|
||||||
|
try:
|
||||||
|
_LOGGER.info("Unloading Olimex ESP32-C5-EVB entry: %s", entry.entry_id)
|
||||||
|
|
||||||
|
# Unregister webhook handler
|
||||||
|
webhook_unregister(hass, entry.entry_id)
|
||||||
|
|
||||||
|
# Unload all platforms
|
||||||
|
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
|
|
||||||
|
if unload_ok:
|
||||||
|
# Clean up data
|
||||||
|
if entry.entry_id in hass.data.get(DOMAIN, {}):
|
||||||
|
hass.data[DOMAIN].pop(entry.entry_id)
|
||||||
|
_LOGGER.debug("Cleaned up data for entry %s", entry.entry_id)
|
||||||
|
|
||||||
|
_LOGGER.info("Successfully unloaded Olimex ESP32-C5-EVB entry: %s", entry.entry_id)
|
||||||
|
return unload_ok
|
||||||
|
except Exception as err:
|
||||||
|
_LOGGER.error("Error unloading Olimex ESP32-C5-EVB entry: %s", err, exc_info=True)
|
||||||
|
return False
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
"""Binary sensor platform for Olimex ESP32-C5-EVB."""
|
||||||
|
import logging
|
||||||
|
from homeassistant.components.binary_sensor import BinarySensorEntity
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
|
|
||||||
|
from .const import DOMAIN, NUM_INPUTS
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up binary sensor entities for inputs."""
|
||||||
|
data = hass.data[DOMAIN][entry.entry_id]
|
||||||
|
|
||||||
|
sensors = [
|
||||||
|
OlimexInputSensor(hass, entry, input_num)
|
||||||
|
for input_num in range(1, NUM_INPUTS + 1)
|
||||||
|
]
|
||||||
|
async_add_entities(sensors, update_before_add=False)
|
||||||
|
|
||||||
|
|
||||||
|
class OlimexInputSensor(BinarySensorEntity):
|
||||||
|
"""Binary sensor for Olimex opto-isolated input.
|
||||||
|
|
||||||
|
State is driven exclusively by webhook POSTs from the board.
|
||||||
|
No polling is performed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_attr_should_poll = False
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant, entry: ConfigEntry, input_num: int):
|
||||||
|
"""Initialize the sensor."""
|
||||||
|
self.hass = hass
|
||||||
|
self._entry = entry
|
||||||
|
self._input_num = input_num
|
||||||
|
self._attr_name = f"Opto Input {input_num}"
|
||||||
|
self._attr_unique_id = f"{entry.entry_id}_input_{input_num}"
|
||||||
|
self._state = False
|
||||||
|
|
||||||
|
async def async_added_to_hass(self):
|
||||||
|
"""Subscribe to webhook dispatcher when entity is added."""
|
||||||
|
signal = f"{DOMAIN}_input_{self._input_num}_event"
|
||||||
|
self.async_on_remove(
|
||||||
|
async_dispatcher_connect(self.hass, signal, self._handle_webhook_event)
|
||||||
|
)
|
||||||
|
await super().async_added_to_hass()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _handle_webhook_event(self, state):
|
||||||
|
"""Handle real-time input event received via webhook from the board."""
|
||||||
|
# Board already inverts opto logic before sending:
|
||||||
|
# state=True means triggered, state=False means not triggered
|
||||||
|
self._state = state
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Input %d webhook event: state=%s (sensor is_on=%s)",
|
||||||
|
self._input_num, state, self._state
|
||||||
|
)
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self):
|
||||||
|
"""Return True if opto-isolated input is triggered."""
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self):
|
||||||
|
"""Return device information."""
|
||||||
|
try:
|
||||||
|
host = self._entry.data.get('host', 'unknown')
|
||||||
|
return {
|
||||||
|
"identifiers": {(DOMAIN, self._entry.entry_id)},
|
||||||
|
"name": f"Olimex ESP32-C5 ({host})",
|
||||||
|
"manufacturer": "Olimex",
|
||||||
|
"model": "ESP32-C5-EVB",
|
||||||
|
}
|
||||||
|
except Exception as err:
|
||||||
|
_LOGGER.debug("Error getting device info: %s", err)
|
||||||
|
return {
|
||||||
|
"identifiers": {(DOMAIN, self._entry.entry_id)},
|
||||||
|
"name": "Olimex ESP32-C5",
|
||||||
|
"manufacturer": "Olimex",
|
||||||
|
"model": "ESP32-C5-EVB",
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
"""Config flow for Olimex ESP32-C5-EVB integration."""
|
||||||
|
import logging
|
||||||
|
import voluptuous as vol
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||||
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
|
||||||
|
from .const import DOMAIN, DEFAULT_PORT, CONF_CALLBACK_IP, DEFAULT_CALLBACK_IP
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class OlimexESP32C5ConfigFlow(config_entries.ConfigFlow, domain="olimex_esp32_c5"):
|
||||||
|
"""Handle a config flow for Olimex ESP32-C5-EVB."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
|
||||||
|
async def async_step_user(self, user_input=None):
|
||||||
|
"""Handle the initial step."""
|
||||||
|
errors = {}
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
host = user_input.get(CONF_HOST, "192.168.0.240")
|
||||||
|
port = user_input.get(CONF_PORT, DEFAULT_PORT)
|
||||||
|
|
||||||
|
# Validate connection to the board
|
||||||
|
try:
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
url = f"http://{host}:{port}/api/status"
|
||||||
|
async with session.get(
|
||||||
|
url,
|
||||||
|
timeout=aiohttp.ClientTimeout(total=10)
|
||||||
|
) as resp:
|
||||||
|
if resp.status != 200:
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
|
else:
|
||||||
|
await self.async_set_unique_id(f"{host}:{port}")
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
|
_LOGGER.info("Successfully connected to Olimex ESP32-C5 at %s", host)
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=f"Olimex ESP32-C5 ({host})",
|
||||||
|
data={
|
||||||
|
CONF_HOST: host,
|
||||||
|
CONF_PORT: port,
|
||||||
|
CONF_CALLBACK_IP: user_input.get(
|
||||||
|
CONF_CALLBACK_IP, DEFAULT_CALLBACK_IP
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
except aiohttp.ClientError:
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
|
_LOGGER.error("Failed to connect to Olimex ESP32-C5 at %s", host)
|
||||||
|
except Exception as err:
|
||||||
|
errors["base"] = "unknown"
|
||||||
|
_LOGGER.error("Error in config flow: %s", err)
|
||||||
|
|
||||||
|
data_schema = vol.Schema({
|
||||||
|
vol.Required(CONF_HOST, default="192.168.0.240"): str,
|
||||||
|
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||||
|
vol.Optional(CONF_CALLBACK_IP, default=DEFAULT_CALLBACK_IP): str,
|
||||||
|
})
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user",
|
||||||
|
data_schema=data_schema,
|
||||||
|
errors=errors,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_import(self, import_data):
|
||||||
|
"""Handle import from YAML configuration."""
|
||||||
|
_LOGGER.debug("Importing Olimex ESP32-C5 from YAML: %s", import_data)
|
||||||
|
host = import_data.get(CONF_HOST, "192.168.0.240")
|
||||||
|
port = import_data.get(CONF_PORT, DEFAULT_PORT)
|
||||||
|
|
||||||
|
# Check if already configured
|
||||||
|
await self.async_set_unique_id(f"{host}:{port}")
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
|
# Validate connection
|
||||||
|
try:
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
url = f"http://{host}:{port}/api/status"
|
||||||
|
async with session.get(
|
||||||
|
url,
|
||||||
|
timeout=aiohttp.ClientTimeout(total=10)
|
||||||
|
) as resp:
|
||||||
|
if resp.status == 200:
|
||||||
|
_LOGGER.info("Successfully imported Olimex ESP32-C5 from YAML at %s", host)
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=f"Olimex ESP32-C5 ({host})",
|
||||||
|
data={
|
||||||
|
CONF_HOST: host,
|
||||||
|
CONF_PORT: port,
|
||||||
|
CONF_CALLBACK_IP: import_data.get(CONF_CALLBACK_IP, DEFAULT_CALLBACK_IP),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
except Exception as err:
|
||||||
|
_LOGGER.error("Failed to import Olimex ESP32-C5 from YAML: %s", err)
|
||||||
|
|
||||||
|
# If validation fails, still create entry but log warning
|
||||||
|
_LOGGER.warning("Could not validate Olimex ESP32-C5 at %s, creating entry anyway", host)
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=f"Olimex ESP32-C5 ({host})",
|
||||||
|
data={
|
||||||
|
CONF_HOST: host,
|
||||||
|
CONF_PORT: port,
|
||||||
|
CONF_CALLBACK_IP: import_data.get(CONF_CALLBACK_IP, DEFAULT_CALLBACK_IP),
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
"""Constants for the Olimex ESP32-C5-EVB integration."""
|
||||||
|
|
||||||
|
DOMAIN = "olimex_esp32_c5"
|
||||||
|
MANUFACTURER = "Olimex"
|
||||||
|
MODEL = "ESP32-C5-EVB"
|
||||||
|
CHIP = "ESP32-C5"
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
CONF_HOST = "host"
|
||||||
|
CONF_PORT = "port"
|
||||||
|
CONF_SCAN_INTERVAL = "scan_interval"
|
||||||
|
CONF_CALLBACK_IP = "callback_ip"
|
||||||
|
|
||||||
|
# Default values
|
||||||
|
DEFAULT_PORT = 80
|
||||||
|
DEFAULT_SCAN_INTERVAL = 5
|
||||||
|
DEFAULT_CALLBACK_IP = "192.168.0.1"
|
||||||
|
|
||||||
|
# Relay and Input info
|
||||||
|
NUM_RELAYS = 2
|
||||||
|
NUM_INPUTS = 2
|
||||||
|
|
||||||
|
# Device info
|
||||||
|
DEVICE_INFO = {
|
||||||
|
"manufacturer": MANUFACTURER,
|
||||||
|
"model": MODEL,
|
||||||
|
"chip": CHIP,
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"domain": "olimex_esp32_c5",
|
||||||
|
"name": "Olimex ESP32-C5-EVB",
|
||||||
|
"codeowners": [],
|
||||||
|
"config_flow": true,
|
||||||
|
"dependencies": [],
|
||||||
|
"documentation": "https://github.com/yourusername/olimex-esp32-c5-ha",
|
||||||
|
"iot_class": "local_polling",
|
||||||
|
"requirements": ["aiohttp>=3.8.0"],
|
||||||
|
"version": "0.1.0"
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"title": "Configure Olimex ESP32-C5-EVB",
|
||||||
|
"description": "Enter the IP address or hostname of your Olimex ESP32-C5-EVB board",
|
||||||
|
"data": {
|
||||||
|
"host": "Host",
|
||||||
|
"port": "Port",
|
||||||
|
"callback_ip": "Home Assistant IP"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "Failed to connect to the board. Check IP address and port.",
|
||||||
|
"unknown": "An unknown error occurred"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
"""Switch platform for Olimex ESP32-C5-EVB."""
|
||||||
|
import logging
|
||||||
|
import aiohttp
|
||||||
|
from homeassistant.components.switch import SwitchEntity
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
|
from .const import DOMAIN, NUM_RELAYS
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Tight timeout for a local LAN device
|
||||||
|
_TIMEOUT = aiohttp.ClientTimeout(total=3)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up switch entities for relays."""
|
||||||
|
data = hass.data[DOMAIN][entry.entry_id]
|
||||||
|
host = data["host"]
|
||||||
|
port = data["port"]
|
||||||
|
|
||||||
|
switches = [
|
||||||
|
OlimexRelaySwitch(entry, host, port, relay_num)
|
||||||
|
for relay_num in range(1, NUM_RELAYS + 1)
|
||||||
|
]
|
||||||
|
async_add_entities(switches, update_before_add=False)
|
||||||
|
|
||||||
|
|
||||||
|
class OlimexRelaySwitch(SwitchEntity):
|
||||||
|
"""Switch for Olimex relay.
|
||||||
|
|
||||||
|
State is set on load via a single GET and on every toggle via the
|
||||||
|
state value returned directly in the POST response — no extra round-trip.
|
||||||
|
A single persistent aiohttp session is reused for all requests.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_attr_should_poll = False
|
||||||
|
|
||||||
|
def __init__(self, entry: ConfigEntry, host: str, port: int, relay_num: int):
|
||||||
|
"""Initialize the switch."""
|
||||||
|
self._entry = entry
|
||||||
|
self._host = host
|
||||||
|
self._port = port
|
||||||
|
self._relay_num = relay_num
|
||||||
|
self._attr_name = f"Relay {relay_num}"
|
||||||
|
self._attr_unique_id = f"{entry.entry_id}_relay_{relay_num}"
|
||||||
|
self._is_on = False
|
||||||
|
self._session: aiohttp.ClientSession | None = None
|
||||||
|
|
||||||
|
async def async_added_to_hass(self):
|
||||||
|
"""Open a persistent HTTP session and fetch initial relay state."""
|
||||||
|
self._session = aiohttp.ClientSession()
|
||||||
|
url = f"http://{self._host}:{self._port}/relay/status?relay={self._relay_num}"
|
||||||
|
try:
|
||||||
|
async with self._session.get(url, timeout=_TIMEOUT) as resp:
|
||||||
|
if resp.status == 200:
|
||||||
|
data = await resp.json()
|
||||||
|
self._is_on = data.get("state", False)
|
||||||
|
except Exception as err:
|
||||||
|
_LOGGER.debug("Relay %d initial fetch failed: %s", self._relay_num, err)
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
async def async_will_remove_from_hass(self):
|
||||||
|
"""Close the HTTP session when entity is removed."""
|
||||||
|
if self._session:
|
||||||
|
await self._session.close()
|
||||||
|
self._session = None
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# SwitchEntity interface
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
async def async_turn_on(self, **kwargs):
|
||||||
|
"""Turn the relay on."""
|
||||||
|
await self._async_set_relay(True)
|
||||||
|
|
||||||
|
async def async_turn_off(self, **kwargs):
|
||||||
|
"""Turn the relay off."""
|
||||||
|
await self._async_set_relay(False)
|
||||||
|
|
||||||
|
async def _async_set_relay(self, on: bool):
|
||||||
|
"""POST on/off to the board; read state from the response body directly."""
|
||||||
|
action = "on" if on else "off"
|
||||||
|
url = f"http://{self._host}:{self._port}/relay/{action}?relay={self._relay_num}"
|
||||||
|
try:
|
||||||
|
async with self._session.post(url, timeout=_TIMEOUT) as resp:
|
||||||
|
if resp.status == 200:
|
||||||
|
data = await resp.json()
|
||||||
|
# Board returns {"status":"ok","state":true/false} — use it directly
|
||||||
|
self._is_on = data.get("state", on)
|
||||||
|
_LOGGER.debug("Relay %d -> %s (board confirmed: %s)", self._relay_num, action, self._is_on)
|
||||||
|
else:
|
||||||
|
_LOGGER.error("Relay %d %s failed: HTTP %d", self._relay_num, action, resp.status)
|
||||||
|
except Exception as err:
|
||||||
|
_LOGGER.error("Relay %d %s error: %s", self._relay_num, action, err)
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self):
|
||||||
|
"""Return True if relay is on."""
|
||||||
|
return self._is_on
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self):
|
||||||
|
"""Return device information."""
|
||||||
|
host = self._entry.data.get("host", "unknown")
|
||||||
|
return {
|
||||||
|
"identifiers": {(DOMAIN, self._entry.entry_id)},
|
||||||
|
"name": f"Olimex ESP32-C5 ({host})",
|
||||||
|
"manufacturer": "Olimex",
|
||||||
|
"model": "ESP32-C5-EVB",
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
"""Webhook handler for Olimex ESP32-C5-EVB input events."""
|
||||||
|
import logging
|
||||||
|
from aiohttp import web
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_input_event(hass: HomeAssistant, webhook_id: str, request) -> web.Response:
|
||||||
|
"""Handle input event webhook from the board."""
|
||||||
|
try:
|
||||||
|
data = await request.json()
|
||||||
|
input_num = data.get("input")
|
||||||
|
state = data.get("state")
|
||||||
|
|
||||||
|
_LOGGER.info("Received input event: input=%s state=%s", input_num, state)
|
||||||
|
|
||||||
|
# Dispatch signal to update binary sensors immediately
|
||||||
|
signal = f"{DOMAIN}_input_{input_num}_event"
|
||||||
|
async_dispatcher_send(hass, signal, state)
|
||||||
|
|
||||||
|
return web.json_response({"status": "ok"})
|
||||||
|
|
||||||
|
except Exception as err:
|
||||||
|
_LOGGER.error("Error handling webhook: %s", err)
|
||||||
|
return web.json_response({"error": str(err)}, status=400)
|
||||||
@@ -0,0 +1,299 @@
|
|||||||
|
# ESP32-C5 Arduino Deployment Guide
|
||||||
|
|
||||||
|
Complete guide to compile and deploy the firmware to your Olimex ESP32-C5-EVB board.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✓ Pre-Deployment Checklist
|
||||||
|
|
||||||
|
- [ ] Arduino IDE installed (version 2.0+)
|
||||||
|
- [ ] ESP32 board package installed (version 3.0.0+)
|
||||||
|
- [ ] Olimex ESP32-C5-EVB board connected via USB
|
||||||
|
- [ ] USB drivers installed for your OS
|
||||||
|
- [ ] WiFi credentials available
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 1: Install Arduino IDE
|
||||||
|
|
||||||
|
1. Download from: https://www.arduino.cc/en/software
|
||||||
|
2. Install and launch **Arduino IDE 2.0 or later**
|
||||||
|
|
||||||
|
**⚠️ Arduino IDE 1.x (including the system package `arduino` on Linux) will NOT work with ESP32 core 3.x.**
|
||||||
|
Core 3.x uses nested variable references in its build system that only Arduino IDE 2.x resolves correctly.
|
||||||
|
Installing `arduino` via `apt` gives you IDE 1.8.19 — download the AppImage/zip directly from arduino.cc instead.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 2: Add ESP32 Board Support
|
||||||
|
|
||||||
|
### Windows/Mac/Linux (same process):
|
||||||
|
|
||||||
|
1. **Open Preferences**
|
||||||
|
- File → Preferences (or Arduino IDE → Settings on Mac)
|
||||||
|
|
||||||
|
2. **Add Board URL**
|
||||||
|
- Find "Additional Boards Manager URLs" field
|
||||||
|
- Add this URL:
|
||||||
|
```
|
||||||
|
https://espressif.github.io/arduino-esp32/package_esp32_index.json
|
||||||
|
```
|
||||||
|
- Click OK
|
||||||
|
|
||||||
|
3. **Install ESP32 Board Package**
|
||||||
|
- Tools → Board → Boards Manager
|
||||||
|
- Search: "esp32"
|
||||||
|
- Install "esp32 by Espressif Systems" (version 3.0.0+)
|
||||||
|
- Wait for installation to complete
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 3: Configure Board Settings
|
||||||
|
|
||||||
|
After installation, configure these exact settings in Arduino IDE:
|
||||||
|
|
||||||
|
### Tools Menu Settings:
|
||||||
|
|
||||||
|
| Setting | Value |
|
||||||
|
|---------|-------|
|
||||||
|
| Board | **ESP32C5 Dev Module** |
|
||||||
|
| USB CDC On Boot | **Enabled** ⚠️ **REQUIRED for Serial Monitor** |
|
||||||
|
| Flash Size | **4MB** |
|
||||||
|
| Flash Frequency | **80 MHz** |
|
||||||
|
| Flash Mode | **QIO** (default for C5) |
|
||||||
|
| PSRAM | Disabled |
|
||||||
|
| CPU Frequency | 240 MHz (WiFi) |
|
||||||
|
| Core Debug Level | None |
|
||||||
|
| Partition Scheme | **No OTA (2MB APP/2MB SPIFFS)** |
|
||||||
|
| Port | `/dev/ttyACM0` (Linux) or `/dev/cu.usbserial-*` (Mac) |
|
||||||
|
|
||||||
|
> **Note:** The ESP32-C5 does not have a "USB Mode" menu. Its USB hardware is fixed (Hardware CDC).
|
||||||
|
> The correct partition scheme for 4 MB flash is **No OTA (2MB APP/2MB SPIFFS)** — this gives 2 MB app space vs 1 MB with `noota_3g`.
|
||||||
|
|
||||||
|
**⚠️ IMPORTANT:**
|
||||||
|
- **USB CDC On Boot MUST be ENABLED** for Serial output to work in Arduino IDE Serial Monitor
|
||||||
|
- The device will show 2 serial ports when USB CDC is enabled:
|
||||||
|
- First port: USB CDC (for Serial.print output)
|
||||||
|
- Second port: USB-UART (legacy, can be ignored)
|
||||||
|
|
||||||
|
**⚠️ Important:** USB CDC On Boot must be **Enabled** for serial output to work!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 4: Install Required Libraries
|
||||||
|
|
||||||
|
The sketch uses these libraries. Install them via Sketch → Include Library → Manage Libraries:
|
||||||
|
|
||||||
|
1. **PN532** (by Elechouse)
|
||||||
|
- Search: "PN532"
|
||||||
|
- Install the official PN532 library by Elechouse
|
||||||
|
- Version: 1.32 or later
|
||||||
|
|
||||||
|
2. Other libraries (WiFi, WebServer, Preferences) are built-in to ESP32 core
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 5: Configure Secrets
|
||||||
|
|
||||||
|
1. Copy `secrets.h.example` to `secrets.h` in the same directory:
|
||||||
|
```bash
|
||||||
|
cp esp32_arduino/secrets.h.example esp32_arduino/secrets.h
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Edit `secrets.h` with your credentials:
|
||||||
|
```cpp
|
||||||
|
const char* WEB_USER = "admin";
|
||||||
|
const char* WEB_PASSWORD = "admin"; // Change this!
|
||||||
|
const char* API_SECRET = ""; // Optional HMAC secret
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Also update WiFi credentials in `esp32_arduino.ino` (around line 58):
|
||||||
|
```cpp
|
||||||
|
const char* ssid = "YOUR_SSID";
|
||||||
|
const char* password = "YOUR_PASSWORD";
|
||||||
|
```
|
||||||
|
|
||||||
|
**⚠️ Never commit `secrets.h` to Git!**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 6: Flash the Board
|
||||||
|
|
||||||
|
### Connect the Board:
|
||||||
|
1. Plug the Olimex ESP32-C5-EVB into your computer via USB
|
||||||
|
2. Wait for USB drivers to load (Windows: ~30 seconds)
|
||||||
|
|
||||||
|
### In Arduino IDE:
|
||||||
|
1. Open `esp32_arduino.ino`
|
||||||
|
2. Tools → Port → Select your board's port
|
||||||
|
- **Linux:** `/dev/ttyUSB0`, `/dev/ttyUSB1`, or `/dev/ACM0`
|
||||||
|
- **macOS:** `/dev/cu.usbserial-*` or `/dev/cu.SLAB_USBtoUART`
|
||||||
|
- **Windows:** `COM3`, `COM4`, etc.
|
||||||
|
|
||||||
|
3. Sketch → Upload (or press Ctrl+U / Cmd+U)
|
||||||
|
|
||||||
|
4. Wait for compilation and flashing to complete (~30 seconds)
|
||||||
|
|
||||||
|
### Expected Output:
|
||||||
|
```
|
||||||
|
Connecting........___....._____
|
||||||
|
esptool.py v4.5.1
|
||||||
|
Serial port COM3
|
||||||
|
Chip is ESP32-C5...
|
||||||
|
Uploading stub...
|
||||||
|
...
|
||||||
|
Leaving...
|
||||||
|
Hard resetting via RTS pin...
|
||||||
|
```
|
||||||
|
|
||||||
|
### If Upload Fails:
|
||||||
|
|
||||||
|
**Problem:** "Failed to connect to ESP32"
|
||||||
|
- **Solution 1:** Try different baud rates (Tools → Upload Speed)
|
||||||
|
- **Solution 2:** Hold USER button during upload
|
||||||
|
- **Solution 3:** Different USB cable or port
|
||||||
|
- **Solution 4:** Update board drivers for your OS
|
||||||
|
|
||||||
|
**Problem:** "No module named 'serial'"
|
||||||
|
- **Solution:** Update Arduino IDE to latest version
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 7: Verify Installation
|
||||||
|
|
||||||
|
After successful upload, the board will reboot.
|
||||||
|
|
||||||
|
### Check Serial Monitor:
|
||||||
|
1. Tools → Serial Monitor (or Ctrl+Shift+M / Cmd+Shift+M)
|
||||||
|
2. Set baud rate to **115200**
|
||||||
|
3. Should see:
|
||||||
|
```
|
||||||
|
=================================
|
||||||
|
ESP32-C5 Home Assistant Device
|
||||||
|
Arduino Framework
|
||||||
|
=================================
|
||||||
|
GPIO initialized
|
||||||
|
Connecting to WiFi: YOUR_SSID (attempt 1/3)
|
||||||
|
...............................
|
||||||
|
✓ WiFi connected!
|
||||||
|
IP : 192.168.0.240
|
||||||
|
RSSI : -42 dBm
|
||||||
|
MAC : xx:xx:xx:xx:xx:xx
|
||||||
|
✓ HTTP server started on port 80
|
||||||
|
=================================
|
||||||
|
Ready! Try these endpoints:
|
||||||
|
http://192.168.0.240/api/status
|
||||||
|
=================================
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test in Browser:
|
||||||
|
1. Open browser and go to: `http://192.168.0.240/`
|
||||||
|
2. Login with credentials from `secrets.h` (default: admin/admin)
|
||||||
|
3. Should see interactive control panel with relays and inputs
|
||||||
|
|
||||||
|
### Test API:
|
||||||
|
```bash
|
||||||
|
curl http://192.168.0.240/api/status
|
||||||
|
# Expected response:
|
||||||
|
# {"input1":true,"input2":true,"relay1":false,"relay2":false,...}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 8: Add to Home Assistant
|
||||||
|
|
||||||
|
Once the board is verified and WiFi works:
|
||||||
|
|
||||||
|
1. In Home Assistant: Settings → Devices & Services
|
||||||
|
2. Click "Create Automation" → Integration
|
||||||
|
3. Search for "Olimex ESP32-C5"
|
||||||
|
4. Click "Olimex ESP32-C5-EVB"
|
||||||
|
5. Enter:
|
||||||
|
- **Host:** 192.168.0.240
|
||||||
|
- **Port:** 80
|
||||||
|
- **Callback IP:** Your Home Assistant server IP (usually 192.168.0.1 or 192.168.0.100)
|
||||||
|
6. Click "Create Entry"
|
||||||
|
|
||||||
|
Home Assistant will automatically:
|
||||||
|
- Discover relays and inputs
|
||||||
|
- Register a webhook for input events
|
||||||
|
- Create switches and binary sensors
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Board doesn't show in Port menu:
|
||||||
|
- Install drivers: https://www.silabs.com/developers/usb-to-uart-bridge-vcp-drivers
|
||||||
|
- Try different USB cable
|
||||||
|
- Check Device Manager (Windows) or System Report (Mac)
|
||||||
|
|
||||||
|
### Serial output garbled:
|
||||||
|
- Check baud rate is **115200** in Serial Monitor
|
||||||
|
- Try different baud rates (9600, 57600)
|
||||||
|
|
||||||
|
### WiFi connection fails:
|
||||||
|
- Verify SSID and password in `esp32_arduino.ino`
|
||||||
|
- Check 2.4 GHz WiFi is enabled (C5 may not support 5 GHz yet)
|
||||||
|
- Power cycle board and router
|
||||||
|
|
||||||
|
### NFC not detected:
|
||||||
|
- Check PN532 is soldered correctly to UEXT connector
|
||||||
|
- Verify TX=GPIO26, RX=GPIO25 wiring
|
||||||
|
- Set PN532 DIP switches to HSU mode (both = 0)
|
||||||
|
- Check serial output for baud detection attempts
|
||||||
|
|
||||||
|
### Can't reach board from Home Assistant:
|
||||||
|
- Verify board IP: check serial monitor output
|
||||||
|
- Ping from HA machine: `ping 192.168.0.240`
|
||||||
|
- Check firewall allows port 80
|
||||||
|
- Ensure HA and board are on same network
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Updating Firmware
|
||||||
|
|
||||||
|
To update code after initial deployment:
|
||||||
|
|
||||||
|
1. Make changes in Arduino IDE
|
||||||
|
2. Edit WiFi credentials if needed
|
||||||
|
3. Sketch → Upload
|
||||||
|
4. Monitor serial output to verify successful flash
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Protecting Your Device
|
||||||
|
|
||||||
|
**Before deployment to production:**
|
||||||
|
|
||||||
|
1. Change default credentials in `secrets.h`:
|
||||||
|
```cpp
|
||||||
|
const char* WEB_USER = "secure_user";
|
||||||
|
const char* WEB_PASSWORD = "strong_password_123";
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Set an API secret for request signing:
|
||||||
|
```cpp
|
||||||
|
const char* API_SECRET = "my_random_secret_key_xyz123";
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Only expose board on trusted networks (private LAN)
|
||||||
|
|
||||||
|
4. Consider firewall rules to restrict access
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- Check [README.md](../README.md) for API endpoints and features
|
||||||
|
- Configure Home Assistant automations with the new switches/sensors
|
||||||
|
- Optional: Add NFC card reader to UEXT connector for access control
|
||||||
|
- Explore [custom_components](../custom_components/) for integration details
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
- Olimex Board: https://github.com/OLIMEX/ESP32-C5-EVB
|
||||||
|
- ESP32 Arduino: https://github.com/espressif/arduino-esp32
|
||||||
|
- PN532 Library: https://github.com/elechouse/PN532
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": "."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"files.exclude": {
|
||||||
|
"**/.git": true,
|
||||||
|
"**/.DS_Store": true,
|
||||||
|
"**/secrets.h": true,
|
||||||
|
"**/*.o": true
|
||||||
|
},
|
||||||
|
"search.exclude": {
|
||||||
|
"**/.git": true,
|
||||||
|
"**/node_modules": true,
|
||||||
|
"**/.venv": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* secrets.h — Credentials & API Keys (NOT Version Controlled)
|
||||||
|
*
|
||||||
|
* Instructions:
|
||||||
|
* 1. Copy this file to secrets.h (same directory)
|
||||||
|
* 2. Edit secrets.h with your WiFi credentials and API secret
|
||||||
|
* 3. DO NOT commit secrets.h to Git
|
||||||
|
* 4. Add secrets.h to .gitignore if not already present
|
||||||
|
* 5. Flash the board with Arduino IDE
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef SECRETS_H
|
||||||
|
#define SECRETS_H
|
||||||
|
|
||||||
|
// ─ WiFi Credentials ───────────────────────────────────────────────────────
|
||||||
|
const char* WIFI_SSID = "YourWiFiSSID";
|
||||||
|
const char* WIFI_PASSWORD = "YourWiFiPassword";
|
||||||
|
|
||||||
|
// ─ Static IP Configuration ────────────────────────────────────────────────
|
||||||
|
// Set USE_STATIC_IP = true to use a fixed IP address
|
||||||
|
// Otherwise, board will request IP via DHCP
|
||||||
|
const bool USE_STATIC_IP = false; // true = static IP; false = DHCP
|
||||||
|
const char* STATIC_IP_ADDR = "192.168.0.240"; // e.g., "192.168.0.240"
|
||||||
|
const char* STATIC_GATEWAY = "192.168.0.1"; // e.g., "192.168.0.1"
|
||||||
|
const char* STATIC_SUBNET = "255.255.255.0"; // e.g., "255.255.255.0"
|
||||||
|
const char* STATIC_DNS1 = "8.8.8.8"; // e.g., "8.8.8.8"
|
||||||
|
const char* STATIC_DNS2 = "8.8.4.4"; // e.g., "8.8.4.4"
|
||||||
|
|
||||||
|
// ─ Web Server Credentials ──────────────────────────────────────────────────
|
||||||
|
const char* WEB_USER = "admin";
|
||||||
|
const char* WEB_PASSWORD = "admin";
|
||||||
|
|
||||||
|
// ─ API Authentication Secret ──────────────────────────────────────────────
|
||||||
|
// Used for HMAC-SHA256 request signing (optional)
|
||||||
|
// If empty, API requests do not require authentication.
|
||||||
|
// To enable, set a random string and include X-Request-Time/X-Request-Sig headers.
|
||||||
|
// Example: "my_super_secret_api_key_12345"
|
||||||
|
const char* API_SECRET = "";
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
NFC Pin Mapping Diagnostic for ESP32-C5-EVB
|
||||||
|
Verifies the UEXT connector GPIO assignments
|
||||||
|
"""
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
DEVICE_IP = sys.argv[1] if len(sys.argv) > 1 else "192.168.0.240"
|
||||||
|
WEB_USER = "Ske087"
|
||||||
|
WEB_PASS = "Matei@123"
|
||||||
|
|
||||||
|
print("=" * 70)
|
||||||
|
print("ESP32-C5-EVB UEXT Pin Mapping Diagnostic")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
# Try to get device info
|
||||||
|
try:
|
||||||
|
resp = requests.get(f"http://{DEVICE_IP}/",
|
||||||
|
auth=(WEB_USER, WEB_PASS),
|
||||||
|
timeout=2)
|
||||||
|
if resp.status_code == 200:
|
||||||
|
print(f"\n✓ Device reachable at {DEVICE_IP}\n")
|
||||||
|
else:
|
||||||
|
print(f"\n✗ Device responded with {resp.status_code}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n✗ Cannot reach device: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Check NFC status
|
||||||
|
try:
|
||||||
|
resp = requests.get(f"http://{DEVICE_IP}/nfc/status",
|
||||||
|
auth=(WEB_USER, WEB_PASS),
|
||||||
|
timeout=2)
|
||||||
|
nfc_status = resp.json()
|
||||||
|
|
||||||
|
print("NFC Module Status:")
|
||||||
|
print(f" Initialized: {nfc_status.get('initialized', False)}")
|
||||||
|
print(f" Enabled: {nfc_status.get('nfc_enabled', False)}")
|
||||||
|
print(f" Access State: {nfc_status.get('access_state', 'unknown')}")
|
||||||
|
|
||||||
|
if not nfc_status.get('initialized'):
|
||||||
|
print("\n⚠️ NFC module NOT initialized!")
|
||||||
|
print("\nDiagnostic Checklist:")
|
||||||
|
print(" 1. Is PN532 physically connected to UEXT connector?")
|
||||||
|
print(" 2. Are DIP switches set to HSU mode (DIP1=0, DIP2=0)?")
|
||||||
|
print(" 3. Check 3.3V power on UEXT Pin 2 (multimeter)")
|
||||||
|
print(" 4. Verify TX/RX wiring:")
|
||||||
|
print(" - UEXT Pin 3 (GPIO26) → PN532 RXD")
|
||||||
|
print(" - UEXT Pin 4 (GPIO25) ← PN532 TXD")
|
||||||
|
print(" 5. Some PN532 boards need 100Ω resistor on TX line")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ Cannot get NFC status: {e}")
|
||||||
|
|
||||||
|
print("\n" + "=" * 70)
|
||||||
|
print("C5-EVB vs C6-EVB UEXT Pin Mapping Reference:")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
print("""
|
||||||
|
C6-EVB (for reference):
|
||||||
|
UEXT Pin 3 → GPIO4 (UART1 TX)
|
||||||
|
UEXT Pin 4 → GPIO5 (UART1 RX)
|
||||||
|
|
||||||
|
C5-EVB (current board):
|
||||||
|
UEXT Pin 3 → GPIO26 (UART1 TX)
|
||||||
|
UEXT Pin 4 → GPIO25 (UART1 RX)
|
||||||
|
UEXT Pin 1 → GND
|
||||||
|
UEXT Pin 2 → +3V3 (power)
|
||||||
|
|
||||||
|
If you previously worked with C6 and connected PN532 to GPIO 4/5,
|
||||||
|
you MUST move it to GPIO 26/25 on the C5!
|
||||||
|
""")
|
||||||
|
print("=" * 70)
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
.Python
|
||||||
|
|
||||||
|
# Home Assistant generated
|
||||||
|
icon.png
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
/esp32_arduino/secrets.h
|
||||||
|
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# Olimex ESP32-C6-EVB — Home Assistant Integration
|
||||||
|
|
||||||
|
This repository contains two components:
|
||||||
|
|
||||||
|
## `custom_components/olimex_esp32_c6`
|
||||||
|
Home Assistant custom integration for the **Olimex ESP32-C6-EVB** board.
|
||||||
|
|
||||||
|
- 4 relay switches (controlled via HTTP POST to the board)
|
||||||
|
- 4 digital inputs (state pushed from board to HA via webhook)
|
||||||
|
- No polling — fully event-driven for inputs, command-driven for relays
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
Copy `custom_components/olimex_esp32_c6` into your Home Assistant `config/custom_components/` directory and restart HA.
|
||||||
|
|
||||||
|
## `esp32_arduino`
|
||||||
|
Arduino sketch for the ESP32-C6-EVB board.
|
||||||
|
|
||||||
|
- Hosts a REST API on port 80
|
||||||
|
- Registers a callback URL with HA on startup
|
||||||
|
- POSTs input state changes to HA webhook in real time
|
||||||
|
|
||||||
|
### Arduino IDE Settings
|
||||||
|
| Setting | Value |
|
||||||
|
|---|---|
|
||||||
|
| Board | ESP32C6 Dev Module |
|
||||||
|
| Flash Size | 4MB |
|
||||||
|
| USB CDC On Boot | Enabled |
|
||||||
|
|
||||||
|
See [`esp32_arduino/DEPLOYMENT_GUIDE.md`](esp32_arduino/DEPLOYMENT_GUIDE.md) for full flashing instructions.
|
||||||
@@ -0,0 +1,310 @@
|
|||||||
|
/**
|
||||||
|
* Olimex ESP32-C6-EVB — Board Functional Test
|
||||||
|
* =================================================
|
||||||
|
* Flash this sketch BEFORE deploying the main firmware.
|
||||||
|
* It tests every hardware subsystem and reports PASS/FAIL
|
||||||
|
* on the Serial Monitor AND on a simple web page.
|
||||||
|
*
|
||||||
|
* Board settings (Arduino IDE):
|
||||||
|
* Board : ESP32C6 Dev Module
|
||||||
|
* USB CDC On Boot : Enabled ← REQUIRED
|
||||||
|
* Flash Size : 4MB
|
||||||
|
* Upload Speed : 921600
|
||||||
|
*
|
||||||
|
* How to read results:
|
||||||
|
* 1. Open Serial Monitor at 115200 baud after upload.
|
||||||
|
* 2. Press RESET on the board — full report prints once.
|
||||||
|
* 3. Connect a phone/PC to the same WiFi and open:
|
||||||
|
* http://192.168.0.181/test (or whatever IP prints)
|
||||||
|
*
|
||||||
|
* Relay self-test: the test pulses each relay 200 ms ON then OFF.
|
||||||
|
* You will see/hear the relays click twice each.
|
||||||
|
*
|
||||||
|
* NFC self-test: tries all baud rates (115200 / 9600 / 57600)
|
||||||
|
* on both pin orientations. Module must be wired on UEXT1.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <WiFi.h>
|
||||||
|
#include <WebServer.h>
|
||||||
|
#include <PN532_HSU.h>
|
||||||
|
#include <PN532.h>
|
||||||
|
|
||||||
|
// ── WiFi credentials ─────────────────────────────────────────────────────────
|
||||||
|
const char* SSID = "BUON GUSTO PARTER";
|
||||||
|
const char* PASSWORD = "arleta13";
|
||||||
|
IPAddress STATIC_IP(192, 168, 0, 181);
|
||||||
|
IPAddress GATEWAY (192, 168, 0, 1);
|
||||||
|
IPAddress SUBNET (255, 255, 255, 0);
|
||||||
|
|
||||||
|
// ── Pin map ───────────────────────────────────────────────────────────────────
|
||||||
|
const int LED_PIN = 8;
|
||||||
|
const int BUT_PIN = 9;
|
||||||
|
const int RELAY_PIN[] = {10, 11, 22, 23}; // Relay 1-4
|
||||||
|
const int INPUT_PIN[] = {1, 2, 3, 15}; // Digital Input 1-4
|
||||||
|
const int NFC_RX = 5; // UEXT1 pin 4
|
||||||
|
const int NFC_TX = 4; // UEXT1 pin 3
|
||||||
|
|
||||||
|
// ── NFC objects ───────────────────────────────────────────────────────────────
|
||||||
|
HardwareSerial nfcSerial(1);
|
||||||
|
PN532_HSU pn532hsu(nfcSerial);
|
||||||
|
PN532 nfc(pn532hsu);
|
||||||
|
|
||||||
|
// ── Web server ────────────────────────────────────────────────────────────────
|
||||||
|
WebServer server(80);
|
||||||
|
|
||||||
|
// ── Test result storage ───────────────────────────────────────────────────────
|
||||||
|
struct TestResult {
|
||||||
|
const char* name;
|
||||||
|
bool passed;
|
||||||
|
String detail;
|
||||||
|
};
|
||||||
|
|
||||||
|
static const int MAX_TESTS = 20;
|
||||||
|
TestResult results[MAX_TESTS];
|
||||||
|
int result_count = 0;
|
||||||
|
int pass_count = 0;
|
||||||
|
int fail_count = 0;
|
||||||
|
|
||||||
|
// ── Helper: record a result ───────────────────────────────────────────────────
|
||||||
|
void record(const char* name, bool ok, String detail = "") {
|
||||||
|
if (result_count < MAX_TESTS) {
|
||||||
|
results[result_count++] = {name, ok, detail};
|
||||||
|
}
|
||||||
|
if (ok) pass_count++; else fail_count++;
|
||||||
|
Serial.printf(" [%s] %s%s\n",
|
||||||
|
ok ? "PASS" : "FAIL",
|
||||||
|
name,
|
||||||
|
detail.length() ? (" — " + detail).c_str() : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// TEST FUNCTIONS
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
void testGPIO() {
|
||||||
|
Serial.println("\n--- GPIO Test ---");
|
||||||
|
|
||||||
|
// ── LED ──────────────────────────────────────────────────────────────────
|
||||||
|
pinMode(LED_PIN, OUTPUT);
|
||||||
|
digitalWrite(LED_PIN, LOW); // LED on (active-low)
|
||||||
|
delay(300);
|
||||||
|
digitalWrite(LED_PIN, HIGH); // LED off
|
||||||
|
delay(100);
|
||||||
|
// We cannot read back an output pin reliably, so just record as attempted.
|
||||||
|
record("LED on/off", true, "GPIO8 — verify LED blinked");
|
||||||
|
|
||||||
|
// ── Button ────────────────────────────────────────────────────────────────
|
||||||
|
pinMode(BUT_PIN, INPUT_PULLUP);
|
||||||
|
int btn = digitalRead(BUT_PIN);
|
||||||
|
// Button is pull-up; HIGH = not pressed. Either state is valid at test time.
|
||||||
|
record("Button readable", true,
|
||||||
|
String("GPIO9 = ") + (btn ? "HIGH (not pressed)" : "LOW (pressed)"));
|
||||||
|
|
||||||
|
// ── Digital Inputs ────────────────────────────────────────────────────────
|
||||||
|
const char* in_names[] = {"Input1 (GPIO1)", "Input2 (GPIO2)",
|
||||||
|
"Input3 (GPIO3)", "Input4 (GPIO15)"};
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
pinMode(INPUT_PIN[i], INPUT_PULLUP);
|
||||||
|
int v = digitalRead(INPUT_PIN[i]);
|
||||||
|
record(in_names[i], true,
|
||||||
|
String("= ") + (v ? "HIGH (open)" : "LOW (active)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Relays ────────────────────────────────────────────────────────────────
|
||||||
|
const char* rel_names[] = {"Relay1 (GPIO10)", "Relay2 (GPIO11)",
|
||||||
|
"Relay3 (GPIO22)", "Relay4 (GPIO23)"};
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
pinMode(RELAY_PIN[i], OUTPUT);
|
||||||
|
digitalWrite(RELAY_PIN[i], LOW);
|
||||||
|
}
|
||||||
|
delay(100);
|
||||||
|
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
// Pulse ON for 200 ms — you should hear/see relay click
|
||||||
|
digitalWrite(RELAY_PIN[i], HIGH);
|
||||||
|
delay(200);
|
||||||
|
digitalWrite(RELAY_PIN[i], LOW);
|
||||||
|
delay(100);
|
||||||
|
record(rel_names[i], true, "pulsed 200 ms — listen for click");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── WiFi ─────────────────────────────────────────────────────────────────────
|
||||||
|
void testWiFi() {
|
||||||
|
Serial.println("\n--- WiFi Test ---");
|
||||||
|
WiFi.disconnect(true);
|
||||||
|
delay(200);
|
||||||
|
WiFi.mode(WIFI_STA);
|
||||||
|
WiFi.config(STATIC_IP, GATEWAY, SUBNET);
|
||||||
|
|
||||||
|
bool connected = false;
|
||||||
|
for (int pass = 1; pass <= 3 && !connected; pass++) {
|
||||||
|
Serial.printf(" Connecting (attempt %d/3)...", pass);
|
||||||
|
WiFi.begin(SSID, PASSWORD);
|
||||||
|
for (int t = 0; t < 40 && WiFi.status() != WL_CONNECTED; t++) {
|
||||||
|
delay(500);
|
||||||
|
Serial.print(".");
|
||||||
|
}
|
||||||
|
Serial.println();
|
||||||
|
connected = (WiFi.status() == WL_CONNECTED);
|
||||||
|
if (!connected && pass < 3) { WiFi.disconnect(true); delay(500); WiFi.mode(WIFI_STA); WiFi.config(STATIC_IP, GATEWAY, SUBNET); }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connected) {
|
||||||
|
record("WiFi connect", true,
|
||||||
|
WiFi.localIP().toString() + " RSSI=" + String(WiFi.RSSI()) + " dBm");
|
||||||
|
} else {
|
||||||
|
record("WiFi connect", false,
|
||||||
|
"status=" + String(WiFi.status()) + " — check SSID/password");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── NFC ──────────────────────────────────────────────────────────────────────
|
||||||
|
void testNFC() {
|
||||||
|
Serial.println("\n--- NFC (PN532 HSU) Test ---");
|
||||||
|
|
||||||
|
const long BAUDS[] = {115200, 9600, 57600, 38400};
|
||||||
|
const int NPINS[2][2]= {{NFC_RX, NFC_TX}, {NFC_TX, NFC_RX}};
|
||||||
|
uint32_t ver = 0;
|
||||||
|
long found_baud = 0;
|
||||||
|
int found_rx = NFC_RX, found_tx = NFC_TX;
|
||||||
|
|
||||||
|
for (int pi = 0; pi < 2 && !ver; pi++) {
|
||||||
|
for (int bi = 0; bi < 4 && !ver; bi++) {
|
||||||
|
int rx = NPINS[pi][0], tx = NPINS[pi][1];
|
||||||
|
Serial.printf(" baud=%-7ld RX=GPIO%d TX=GPIO%d ... ", BAUDS[bi], rx, tx);
|
||||||
|
nfcSerial.begin(BAUDS[bi], SERIAL_8N1, rx, tx);
|
||||||
|
delay(500);
|
||||||
|
nfc.begin();
|
||||||
|
ver = nfc.getFirmwareVersion();
|
||||||
|
if (ver) {
|
||||||
|
found_baud = BAUDS[bi]; found_rx = rx; found_tx = tx;
|
||||||
|
Serial.println("FOUND");
|
||||||
|
} else {
|
||||||
|
Serial.println("no response");
|
||||||
|
delay(100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ver) {
|
||||||
|
nfc.SAMConfig();
|
||||||
|
char detail[80];
|
||||||
|
snprintf(detail, sizeof(detail),
|
||||||
|
"PN5%02X FW=%d.%d baud=%ld RX=GPIO%d TX=GPIO%d",
|
||||||
|
(ver >> 24) & 0xFF,
|
||||||
|
(ver >> 16) & 0xFF, (ver >> 8) & 0xFF,
|
||||||
|
found_baud, found_rx, found_tx);
|
||||||
|
record("NFC PN532 init", true, detail);
|
||||||
|
} else {
|
||||||
|
record("NFC PN532 init", false,
|
||||||
|
"not detected — check DIP switches (both=0) & UEXT1 wiring");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// WEB PAGE GET /test
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
void handleTestPage() {
|
||||||
|
String h = "<!DOCTYPE html><html><head><title>Board Test</title>";
|
||||||
|
h += "<meta name='viewport' content='width=device-width,initial-scale=1'>";
|
||||||
|
h += "<style>";
|
||||||
|
h += "body{font-family:monospace;margin:24px;background:#1a1a1a;color:#eee}";
|
||||||
|
h += "h1{font-size:20px;color:#fff;margin-bottom:4px}";
|
||||||
|
h += ".sub{color:#888;font-size:13px;margin-bottom:20px}";
|
||||||
|
h += "table{border-collapse:collapse;width:100%;max-width:700px}";
|
||||||
|
h += "th{background:#333;padding:8px 12px;text-align:left;font-size:13px;color:#aaa}";
|
||||||
|
h += "td{padding:8px 12px;border-bottom:1px solid #333;font-size:13px}";
|
||||||
|
h += ".pass{color:#4CAF50;font-weight:bold}.fail{color:#f44336;font-weight:bold}";
|
||||||
|
h += ".detail{color:#aaa;font-size:12px}";
|
||||||
|
h += ".summary{margin-top:16px;padding:12px;border-radius:6px;font-size:15px}";
|
||||||
|
h += ".ok{background:#1b5e20;color:#a5d6a7}.bad{background:#b71c1c;color:#ffcdd2}";
|
||||||
|
h += "</style></head><body>";
|
||||||
|
h += "<h1>Olimex ESP32-C6-EVB — Functional Test</h1>";
|
||||||
|
h += "<div class='sub'>MAC: " + WiFi.macAddress() + " IP: "
|
||||||
|
+ WiFi.localIP().toString() + " Uptime: "
|
||||||
|
+ String(millis() / 1000) + "s</div>";
|
||||||
|
|
||||||
|
h += "<table><tr><th>#</th><th>Test</th><th>Result</th><th>Detail</th></tr>";
|
||||||
|
for (int i = 0; i < result_count; i++) {
|
||||||
|
bool ok = results[i].passed;
|
||||||
|
h += "<tr><td>" + String(i + 1) + "</td>";
|
||||||
|
h += "<td>" + String(results[i].name) + "</td>";
|
||||||
|
h += "<td class='" + String(ok ? "pass" : "fail") + "'>"
|
||||||
|
+ String(ok ? "PASS" : "FAIL") + "</td>";
|
||||||
|
h += "<td class='detail'>" + results[i].detail + "</td></tr>";
|
||||||
|
}
|
||||||
|
h += "</table>";
|
||||||
|
|
||||||
|
bool all_ok = (fail_count == 0);
|
||||||
|
h += "<div class='summary " + String(all_ok ? "ok" : "bad") + "'>";
|
||||||
|
h += String(pass_count) + " PASSED / "
|
||||||
|
+ String(fail_count) + " FAILED out of "
|
||||||
|
+ String(result_count) + " tests";
|
||||||
|
if (all_ok) h += " ✓ Board OK";
|
||||||
|
else h += " ✗ Check failures above";
|
||||||
|
h += "</div></body></html>";
|
||||||
|
server.send(200, "text/html", h);
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleTestJSON() {
|
||||||
|
String j = "{\"pass\":" + String(pass_count)
|
||||||
|
+ ",\"fail\":" + String(fail_count)
|
||||||
|
+ ",\"total\":" + String(result_count)
|
||||||
|
+ ",\"board_ok\":" + String(fail_count == 0 ? "true" : "false")
|
||||||
|
+ ",\"mac\":\"" + WiFi.macAddress() + "\""
|
||||||
|
+ ",\"ip\":\"" + WiFi.localIP().toString() + "\""
|
||||||
|
+ ",\"uptime_s\":" + String(millis() / 1000)
|
||||||
|
+ ",\"tests\":[";
|
||||||
|
for (int i = 0; i < result_count; i++) {
|
||||||
|
if (i) j += ",";
|
||||||
|
j += "{\"name\":\"" + String(results[i].name) + "\""
|
||||||
|
+ ",\"pass\":" + String(results[i].passed ? "true" : "false")
|
||||||
|
+ ",\"detail\":\"" + results[i].detail + "\"}";
|
||||||
|
}
|
||||||
|
j += "]}";
|
||||||
|
server.send(200, "application/json", j);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// setup / loop
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
Serial.begin(115200);
|
||||||
|
delay(2000);
|
||||||
|
for (int i = 0; i < 10 && !Serial; i++) delay(500);
|
||||||
|
|
||||||
|
Serial.println("\n\n╔══════════════════════════════════════╗");
|
||||||
|
Serial.println("║ Olimex ESP32-C6-EVB Board Test ║");
|
||||||
|
Serial.println("╚══════════════════════════════════════╝");
|
||||||
|
|
||||||
|
testGPIO();
|
||||||
|
testWiFi();
|
||||||
|
testNFC();
|
||||||
|
|
||||||
|
// ── Start web server ──────────────────────────────────────────────────────
|
||||||
|
server.on("/", HTTP_GET, handleTestPage);
|
||||||
|
server.on("/test", HTTP_GET, handleTestPage);
|
||||||
|
server.on("/test.json", HTTP_GET, handleTestJSON);
|
||||||
|
server.onNotFound([](){ server.send(404, "text/plain", "use /test or /test.json"); });
|
||||||
|
server.begin();
|
||||||
|
|
||||||
|
// ── Final summary on serial ───────────────────────────────────────────────
|
||||||
|
Serial.println("\n╔══════════════════════════════════════╗");
|
||||||
|
Serial.printf( "║ PASSED: %2d FAILED: %2d TOTAL: %2d ║\n",
|
||||||
|
pass_count, fail_count, result_count);
|
||||||
|
Serial.println(fail_count == 0
|
||||||
|
? "║ ✓ ALL TESTS PASSED — board is OK ║"
|
||||||
|
: "║ ✗ FAILURES DETECTED — see above ║");
|
||||||
|
Serial.println("╚══════════════════════════════════════╝");
|
||||||
|
if (WiFi.status() == WL_CONNECTED) {
|
||||||
|
Serial.printf("\nOpen browser: http://%s/test\n", WiFi.localIP().toString().c_str());
|
||||||
|
Serial.printf("Or fetch JSON: http://%s/test.json\n\n", WiFi.localIP().toString().c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
server.handleClient();
|
||||||
|
}
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Olimex ESP32-C6-EVB — Remote Board Verification Script
|
||||||
|
=========================================================
|
||||||
|
Queries the board's REST API and verifies every subsystem.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 board_verify.py # uses default IP 192.168.0.181
|
||||||
|
python3 board_verify.py 192.168.0.200 # custom IP
|
||||||
|
python3 board_verify.py --json # machine-readable output
|
||||||
|
|
||||||
|
Requirements: pip install requests (already in location_managemet requirements)
|
||||||
|
|
||||||
|
What it tests:
|
||||||
|
1. Board reachability (GET /api/status)
|
||||||
|
2. All 4 relays (POST /relay/on, GET /relay/status, POST /relay/off)
|
||||||
|
3. All 4 digital inputs (GET /input/status)
|
||||||
|
4. LED (POST /led/on + /led/off)
|
||||||
|
5. NFC reader (GET /nfc/status)
|
||||||
|
6. NFC config API (GET /nfc/config)
|
||||||
|
|
||||||
|
NOTE: Relay tests cycle each relay ON→verify→OFF→verify.
|
||||||
|
You should hear/see the relay click during the test.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import argparse
|
||||||
|
import requests
|
||||||
|
|
||||||
|
TIMEOUT = 5 # seconds per HTTP request
|
||||||
|
RELAY_DLY = 0.4 # seconds to wait between relay on/status/off
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Result tracking
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
def record(name: str, ok: bool, detail: str = "") -> bool:
|
||||||
|
results.append({"name": name, "pass": ok, "detail": detail})
|
||||||
|
icon = "\033[32m[PASS]\033[0m" if ok else "\033[31m[FAIL]\033[0m"
|
||||||
|
print(f" {icon} {name}" + (f" — {detail}" if detail else ""))
|
||||||
|
return ok
|
||||||
|
|
||||||
|
|
||||||
|
def _get(url: str):
|
||||||
|
try:
|
||||||
|
r = requests.get(url, timeout=TIMEOUT)
|
||||||
|
r.raise_for_status()
|
||||||
|
return r.json()
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
return {"_error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
def _post(url: str):
|
||||||
|
try:
|
||||||
|
r = requests.post(url, timeout=TIMEOUT)
|
||||||
|
r.raise_for_status()
|
||||||
|
return r.json()
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
return {"_error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Tests
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_reachability(base: str) -> bool:
|
||||||
|
print("\n── Connectivity ──────────────────────────────")
|
||||||
|
data = _get(f"{base}/api/status")
|
||||||
|
if data is None:
|
||||||
|
record("Board reachable", False, f"no response from {base}")
|
||||||
|
return False
|
||||||
|
if "_error" in data:
|
||||||
|
record("Board reachable", False, data["_error"])
|
||||||
|
return False
|
||||||
|
|
||||||
|
record("Board reachable", True,
|
||||||
|
f"IP {base.split('//')[1]} "
|
||||||
|
f"nfc_init={data.get('nfc_initialized','?')} "
|
||||||
|
f"nfc_uid={data.get('nfc_last_uid') or '(none)'}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def test_relays(base: str):
|
||||||
|
print("\n── Relay Tests ───────────────────────────────")
|
||||||
|
for relay in range(1, 5):
|
||||||
|
# Turn ON
|
||||||
|
r_on = _post(f"{base}/relay/on?relay={relay}")
|
||||||
|
if r_on is None:
|
||||||
|
record(f"Relay {relay} ON", False, "no response"); continue
|
||||||
|
time.sleep(RELAY_DLY)
|
||||||
|
|
||||||
|
# Verify state = true
|
||||||
|
r_st = _get(f"{base}/relay/status?relay={relay}")
|
||||||
|
on_ok = r_st is not None and r_st.get("state") is True
|
||||||
|
record(f"Relay {relay} ON", on_ok,
|
||||||
|
("state=true" if on_ok else f"got {r_st}"))
|
||||||
|
|
||||||
|
time.sleep(RELAY_DLY)
|
||||||
|
|
||||||
|
# Turn OFF
|
||||||
|
_post(f"{base}/relay/off?relay={relay}")
|
||||||
|
time.sleep(RELAY_DLY)
|
||||||
|
|
||||||
|
# Verify state = false
|
||||||
|
r_st2 = _get(f"{base}/relay/status?relay={relay}")
|
||||||
|
off_ok = r_st2 is not None and r_st2.get("state") is False
|
||||||
|
record(f"Relay {relay} OFF", off_ok,
|
||||||
|
("state=false" if off_ok else f"got {r_st2}"))
|
||||||
|
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_inputs(base: str):
|
||||||
|
print("\n── Digital Input Tests ───────────────────────")
|
||||||
|
for inp in range(1, 5):
|
||||||
|
data = _get(f"{base}/input/status?input={inp}")
|
||||||
|
if data is None:
|
||||||
|
record(f"Input {inp} readable", False, "no response"); continue
|
||||||
|
if "_error" in data:
|
||||||
|
record(f"Input {inp} readable", False, data["_error"]); continue
|
||||||
|
state = data.get("state")
|
||||||
|
record(f"Input {inp} readable", state is not None,
|
||||||
|
f"state={'HIGH' if state else 'LOW'}" if state is not None else f"got {data}")
|
||||||
|
|
||||||
|
|
||||||
|
def test_led(base: str):
|
||||||
|
print("\n── LED Test ──────────────────────────────────")
|
||||||
|
on_r = _post(f"{base}/led/on")
|
||||||
|
time.sleep(0.3)
|
||||||
|
off_r = _post(f"{base}/led/off")
|
||||||
|
led_ok = (on_r is not None and "status" in on_r
|
||||||
|
and off_r is not None and "status" in off_r)
|
||||||
|
record("LED on/off", led_ok,
|
||||||
|
"API responded OK — verify LED blinked" if led_ok else f"on={on_r} off={off_r}")
|
||||||
|
|
||||||
|
|
||||||
|
def test_nfc(base: str):
|
||||||
|
print("\n── NFC Test ──────────────────────────────────")
|
||||||
|
data = _get(f"{base}/nfc/status")
|
||||||
|
if data is None:
|
||||||
|
record("NFC endpoint", False, f"GET /nfc/status no response"); return
|
||||||
|
if "_error" in data:
|
||||||
|
record("NFC endpoint", False, data["_error"]); return
|
||||||
|
|
||||||
|
record("NFC endpoint reachable", True, "")
|
||||||
|
|
||||||
|
init = data.get("initialized", False)
|
||||||
|
record("NFC PN532 initialized", init,
|
||||||
|
f"last_uid={data.get('last_uid') or '(none)'} "
|
||||||
|
f"access_state={data.get('access_state','?')}" if init
|
||||||
|
else "PN532 not detected — check hardware")
|
||||||
|
|
||||||
|
# Config endpoint
|
||||||
|
cfg = _get(f"{base}/nfc/config")
|
||||||
|
if cfg and "_error" not in cfg:
|
||||||
|
record("NFC config endpoint", True,
|
||||||
|
f"auth_uid='{cfg.get('auth_uid') or 'any'}' "
|
||||||
|
f"relay={cfg.get('relay_num')} "
|
||||||
|
f"pulse={cfg.get('pulse_ms')} ms")
|
||||||
|
else:
|
||||||
|
record("NFC config endpoint", False, str(cfg))
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Optional: read board_test sketch results directly
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_sketch_results(base: str):
|
||||||
|
"""If the board_test sketch is running it exposes /test.json — use it."""
|
||||||
|
data = _get(f"{base}/test.json")
|
||||||
|
if data is None or "_error" in data:
|
||||||
|
return # main firmware running — no /test.json endpoint
|
||||||
|
print("\n── Board-Test Sketch Results (from /test.json) ──")
|
||||||
|
for t in data.get("tests", []):
|
||||||
|
record(f"[sketch] {t['name']}", t["pass"], t.get("detail", ""))
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Entry point
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Olimex ESP32-C6-EVB board verifier")
|
||||||
|
parser.add_argument("ip", nargs="?", default="192.168.0.181",
|
||||||
|
help="Board IP address (default: 192.168.0.181)")
|
||||||
|
parser.add_argument("--json", action="store_true",
|
||||||
|
help="Output results as JSON")
|
||||||
|
parser.add_argument("--skip-relays", action="store_true",
|
||||||
|
help="Skip relay tests (no load wired)")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
base = f"http://{args.ip}"
|
||||||
|
|
||||||
|
print(f"\n╔══════════════════════════════════════════╗")
|
||||||
|
print(f"║ Olimex ESP32-C6-EVB Remote Verifier ║")
|
||||||
|
print(f"╚══════════════════════════════════════════╝")
|
||||||
|
print(f" Target: {base}\n")
|
||||||
|
|
||||||
|
# Try board_test sketch first (if deployed)
|
||||||
|
test_sketch_results(base)
|
||||||
|
|
||||||
|
# Connectivity gate — abort if board unreachable
|
||||||
|
if not test_reachability(base):
|
||||||
|
print("\n\033[31m Board unreachable — aborting remaining tests.\033[0m")
|
||||||
|
print(f" Try: ping {args.ip} or wget -qO- {base}/api/status\n")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if not args.skip_relays:
|
||||||
|
test_relays(base)
|
||||||
|
else:
|
||||||
|
print("\n── Relay Tests SKIPPED (--skip-relays) ──────")
|
||||||
|
|
||||||
|
test_inputs(base)
|
||||||
|
test_led(base)
|
||||||
|
test_nfc(base)
|
||||||
|
|
||||||
|
# ── Summary ───────────────────────────────────────────────────────────────
|
||||||
|
passed = sum(1 for r in results if r["pass"])
|
||||||
|
failed = sum(1 for r in results if not r["pass"])
|
||||||
|
total = len(results)
|
||||||
|
all_ok = failed == 0
|
||||||
|
|
||||||
|
print(f"\n╔══════════════════════════════════════════╗")
|
||||||
|
print(f"║ PASSED: {passed:2d} FAILED: {failed:2d} TOTAL: {total:2d} ║")
|
||||||
|
print("║ \033[32m✓ ALL TESTS PASSED — board is OK\033[0m ║" if all_ok else
|
||||||
|
"║ \033[31m✗ FAILURES DETECTED — see above\033[0m ║")
|
||||||
|
print(f"╚══════════════════════════════════════════╝\n")
|
||||||
|
|
||||||
|
if args.json:
|
||||||
|
summary = {
|
||||||
|
"board_ip": args.ip,
|
||||||
|
"pass": passed,
|
||||||
|
"fail": failed,
|
||||||
|
"total": total,
|
||||||
|
"board_ok": all_ok,
|
||||||
|
"tests": results,
|
||||||
|
}
|
||||||
|
print(json.dumps(summary, indent=2))
|
||||||
|
|
||||||
|
sys.exit(0 if all_ok else 1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
# Olimex ESP32-C6-EVB Home Assistant Integration
|
||||||
|
|
||||||
|
This is a custom integration for the **Olimex ESP32-C6-EVB** board featuring the **Espressif ESP32-C6 WROOM-1** chip.
|
||||||
|
|
||||||
|
## Board Specifications
|
||||||
|
|
||||||
|
- **Manufacturer**: Olimex
|
||||||
|
- **Model**: ESP32-C6-EVB
|
||||||
|
- **Chip**: ESP32-C6 WROOM-1
|
||||||
|
- **Features**:
|
||||||
|
- Wi-Fi 6 (802.11ax)
|
||||||
|
- Bluetooth 5.3 (LE)
|
||||||
|
- RISC-V 32-bit single-core processor
|
||||||
|
- Multiple I/O pins
|
||||||
|
- Relays and GPIO control
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Copy the `olimex_esp32_c6` folder to your `custom_components` directory
|
||||||
|
2. Restart Home Assistant
|
||||||
|
3. Add the integration through the UI: Configuration → Integrations → Add Integration → "Olimex ESP32-C6-EVB"
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Enter the IP address and port (default 80) of your ESP32-C6-EVB board.
|
||||||
|
|
||||||
|
## ESP32 Firmware Development
|
||||||
|
|
||||||
|
### Required API Endpoints
|
||||||
|
|
||||||
|
Your ESP32 firmware should implement these HTTP endpoints:
|
||||||
|
|
||||||
|
#### Status Endpoint
|
||||||
|
```
|
||||||
|
GET http://<IP>:<PORT>/api/status
|
||||||
|
Response: {
|
||||||
|
"temperature": 25.5,
|
||||||
|
"wifi_rssi": -45
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Relay Control
|
||||||
|
```
|
||||||
|
POST http://<IP>:<PORT>/api/relay/<relay_id>/on
|
||||||
|
POST http://<IP>:<PORT>/api/relay/<relay_id>/off
|
||||||
|
GET http://<IP>:<PORT>/api/relay/<relay_id>/status
|
||||||
|
Response: {"state": true}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### LED Control
|
||||||
|
```
|
||||||
|
POST http://<IP>:<PORT>/api/led/<led_id>/on
|
||||||
|
POST http://<IP>:<PORT>/api/led/<led_id>/off
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development Tools
|
||||||
|
|
||||||
|
- **ESP-IDF**: Espressif's official IoT Development Framework
|
||||||
|
- **Arduino IDE**: With ESP32 board support
|
||||||
|
- **PlatformIO**: Advanced IDE for embedded development
|
||||||
|
|
||||||
|
### Example Arduino Sketch Structure
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <WiFi.h>
|
||||||
|
#include <WebServer.h>
|
||||||
|
|
||||||
|
WebServer server(80);
|
||||||
|
|
||||||
|
void handleStatus() {
|
||||||
|
String json = "{\"temperature\": 25.5, \"wifi_rssi\": " + String(WiFi.RSSI()) + "}";
|
||||||
|
server.send(200, "application/json", json);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
WiFi.begin("SSID", "PASSWORD");
|
||||||
|
server.on("/api/status", handleStatus);
|
||||||
|
server.begin();
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
server.handleClient();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Temperature monitoring
|
||||||
|
- WiFi signal strength
|
||||||
|
- Relay control
|
||||||
|
- LED control
|
||||||
|
- Extensible for GPIO, ADC, and other peripherals
|
||||||
|
|
||||||
|
## TODO
|
||||||
|
|
||||||
|
- [ ] Implement actual ESP32 firmware with REST API
|
||||||
|
- [ ] Add support for more sensors
|
||||||
|
- [ ] Add button entities for GPIO inputs
|
||||||
|
- [ ] Implement OTA updates
|
||||||
|
- [ ] Add MQTT support as alternative to HTTP
|
||||||
|
- [ ] Add ESPHome configuration option
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [Olimex ESP32-C6-EVB Documentation](https://www.olimex.com/Products/IoT/ESP32-C6/ESP32-C6-EVB/)
|
||||||
|
- [ESP32-C6 Technical Reference](https://www.espressif.com/en/products/socs/esp32-c6)
|
||||||
|
- [Home Assistant Custom Integration Documentation](https://developers.home-assistant.io/)
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
# Olimex ESP32-C6-EVB Home Assistant Integration Setup Guide
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This Home Assistant integration automatically manages the Olimex ESP32-C6-EVB board configuration through the UI.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Binary Sensors** - Monitor 4 digital inputs (DIN1-DIN4)
|
||||||
|
- **Template Switches** - Control 4 relays (REL1-REL4)
|
||||||
|
- **Auto-discovery** - Configurable via config flow UI (no YAML required)
|
||||||
|
- **Connection validation** - Verifies board is online before saving config
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### 1. Deploy Arduino Firmware
|
||||||
|
|
||||||
|
First, ensure your board is running the updated Arduino firmware with all 4 relays and inputs configured:
|
||||||
|
|
||||||
|
- Deploy: `/srv/homeassist/esp32_arduino/esp32_arduino.ino`
|
||||||
|
- Board: Olimex ESP32-C6-EVB
|
||||||
|
- Firmware includes:
|
||||||
|
- Static IP: `192.168.0.181`
|
||||||
|
- 4 Relays (GPIO 10, 11, 22, 23)
|
||||||
|
- 4 Inputs (GPIO 1, 2, 3, 15) with pull-ups
|
||||||
|
|
||||||
|
### 2. Setup Integration via Home Assistant UI
|
||||||
|
|
||||||
|
1. Go to **Settings** → **Devices & Services**
|
||||||
|
2. Click **Create Integration** (+ button)
|
||||||
|
3. Search for **"Olimex"**
|
||||||
|
4. Click **Olimex ESP32-C6-EVB**
|
||||||
|
5. Enter configuration:
|
||||||
|
- **Device IP Address**: `192.168.0.181` (default)
|
||||||
|
- **Port**: `80` (default)
|
||||||
|
- **Scan Interval**: `5` seconds (default)
|
||||||
|
6. Click **Submit**
|
||||||
|
|
||||||
|
The integration will:
|
||||||
|
- ✅ Verify connection to the board
|
||||||
|
- ✅ Create 4 binary sensors for inputs
|
||||||
|
- ✅ Set up the data coordinator for relay polling
|
||||||
|
|
||||||
|
### 3. Create Template Switches (Manual for now)
|
||||||
|
|
||||||
|
For now, you still need to add this to your `configuration.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
template:
|
||||||
|
- switch:
|
||||||
|
- name: "Relay 1"
|
||||||
|
unique_id: "olimex_relay_1"
|
||||||
|
state: "{{ state_attr('sensor.relay_1_status', 'state') | default('off') }}"
|
||||||
|
turn_on:
|
||||||
|
service: rest_command.relay_1_on
|
||||||
|
turn_off:
|
||||||
|
service: rest_command.relay_1_off
|
||||||
|
|
||||||
|
- name: "Relay 2"
|
||||||
|
unique_id: "olimex_relay_2"
|
||||||
|
state: "{{ state_attr('sensor.relay_2_status', 'state') | default('off') }}"
|
||||||
|
turn_on:
|
||||||
|
service: rest_command.relay_2_on
|
||||||
|
turn_off:
|
||||||
|
service: rest_command.relay_2_off
|
||||||
|
|
||||||
|
- name: "Relay 3"
|
||||||
|
unique_id: "olimex_relay_3"
|
||||||
|
state: "{{ state_attr('sensor.relay_3_status', 'state') | default('off') }}"
|
||||||
|
turn_on:
|
||||||
|
service: rest_command.relay_3_on
|
||||||
|
turn_off:
|
||||||
|
service: rest_command.relay_3_off
|
||||||
|
|
||||||
|
- name: "Relay 4"
|
||||||
|
unique_id: "olimex_relay_4"
|
||||||
|
state: "{{ state_attr('sensor.relay_4_status', 'state') | default('off') }}"
|
||||||
|
turn_on:
|
||||||
|
service: rest_command.relay_4_on
|
||||||
|
turn_off:
|
||||||
|
service: rest_command.relay_4_off
|
||||||
|
|
||||||
|
sensor:
|
||||||
|
- platform: rest
|
||||||
|
name: "Relay 1 Status"
|
||||||
|
unique_id: "sensor_relay_1_status"
|
||||||
|
resource: "http://192.168.0.181/relay/status?relay=1"
|
||||||
|
value_template: "{{ value_json.state }}"
|
||||||
|
scan_interval: 5
|
||||||
|
|
||||||
|
- platform: rest
|
||||||
|
name: "Relay 2 Status"
|
||||||
|
unique_id: "sensor_relay_2_status"
|
||||||
|
resource: "http://192.168.0.181/relay/status?relay=2"
|
||||||
|
value_template: "{{ value_json.state }}"
|
||||||
|
scan_interval: 5
|
||||||
|
|
||||||
|
- platform: rest
|
||||||
|
name: "Relay 3 Status"
|
||||||
|
unique_id: "sensor_relay_3_status"
|
||||||
|
resource: "http://192.168.0.181/relay/status?relay=3"
|
||||||
|
value_template: "{{ value_json.state }}"
|
||||||
|
scan_interval: 5
|
||||||
|
|
||||||
|
- platform: rest
|
||||||
|
name: "Relay 4 Status"
|
||||||
|
unique_id: "sensor_relay_4_status"
|
||||||
|
resource: "http://192.168.0.181/relay/status?relay=4"
|
||||||
|
value_template: "{{ value_json.state }}"
|
||||||
|
scan_interval: 5
|
||||||
|
|
||||||
|
rest_command:
|
||||||
|
relay_1_on:
|
||||||
|
url: "http://192.168.0.181/relay/on?relay=1"
|
||||||
|
method: POST
|
||||||
|
relay_1_off:
|
||||||
|
url: "http://192.168.0.181/relay/off?relay=1"
|
||||||
|
method: POST
|
||||||
|
relay_2_on:
|
||||||
|
url: "http://192.168.0.181/relay/on?relay=2"
|
||||||
|
method: POST
|
||||||
|
relay_2_off:
|
||||||
|
url: "http://192.168.0.181/relay/off?relay=2"
|
||||||
|
method: POST
|
||||||
|
relay_3_on:
|
||||||
|
url: "http://192.168.0.181/relay/on?relay=3"
|
||||||
|
method: POST
|
||||||
|
relay_3_off:
|
||||||
|
url: "http://192.168.0.181/relay/off?relay=3"
|
||||||
|
method: POST
|
||||||
|
relay_4_on:
|
||||||
|
url: "http://192.168.0.181/relay/on?relay=4"
|
||||||
|
method: POST
|
||||||
|
relay_4_off:
|
||||||
|
url: "http://192.168.0.181/relay/off?relay=4"
|
||||||
|
method: POST
|
||||||
|
```
|
||||||
|
|
||||||
|
Then restart Home Assistant.
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
The board provides these REST API endpoints:
|
||||||
|
|
||||||
|
### Relay Control
|
||||||
|
- `POST /relay/on?relay=1-4` - Turn relay on
|
||||||
|
- `POST /relay/off?relay=1-4` - Turn relay off
|
||||||
|
- `GET /relay/status?relay=1-4` - Get relay state (returns `{"state": true/false}`)
|
||||||
|
|
||||||
|
### Input Reading
|
||||||
|
- `GET /input/status?input=1-4` - Read input state (returns `{"state": true/false}`)
|
||||||
|
|
||||||
|
### Device Status
|
||||||
|
- `GET /api/status` - Get overall device status
|
||||||
|
|
||||||
|
## Entities Created
|
||||||
|
|
||||||
|
### Binary Sensors (Inputs)
|
||||||
|
- `binary_sensor.input_1` - Digital Input 1 (GPIO 1)
|
||||||
|
- `binary_sensor.input_2` - Digital Input 2 (GPIO 2)
|
||||||
|
- `binary_sensor.input_3` - Digital Input 3 (GPIO 3)
|
||||||
|
- `binary_sensor.input_4` - Digital Input 4 (GPIO 15)
|
||||||
|
|
||||||
|
### Switches (Relays) - via template
|
||||||
|
- `switch.relay_1` - Relay 1 (GPIO 10)
|
||||||
|
- `switch.relay_2` - Relay 2 (GPIO 11)
|
||||||
|
- `switch.relay_3` - Relay 3 (GPIO 22)
|
||||||
|
- `switch.relay_4` - Relay 4 (GPIO 23)
|
||||||
|
|
||||||
|
### Sensors (Status)
|
||||||
|
- `sensor.relay_1_status` - Relay 1 state polling
|
||||||
|
- `sensor.relay_2_status` - Relay 2 state polling
|
||||||
|
- `sensor.relay_3_status` - Relay 3 state polling
|
||||||
|
- `sensor.relay_4_status` - Relay 4 state polling
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Integration won't add
|
||||||
|
- Check board IP is correct (default: `192.168.0.181`)
|
||||||
|
- Verify board is online and connected to WiFi
|
||||||
|
- Check Home Assistant logs: `Settings > System > Logs`
|
||||||
|
|
||||||
|
### Inputs not showing
|
||||||
|
- Verify Arduino firmware is deployed
|
||||||
|
- Check GPIO pin configuration matches board
|
||||||
|
- Inputs show as binary_sensor entities after integration setup
|
||||||
|
|
||||||
|
### Relays not working
|
||||||
|
- Ensure relay REST sensors are created in configuration.yaml
|
||||||
|
- Verify REST commands point to correct IP/port
|
||||||
|
- Check board serial output for API activity
|
||||||
|
|
||||||
|
### Slow response time
|
||||||
|
- Increase scan interval (default is 5 seconds)
|
||||||
|
- Can be changed in integration options
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
- [ ] Auto-create relay switches without YAML
|
||||||
|
- [ ] Add sensor polling via coordinator
|
||||||
|
- [ ] Support multiple boards
|
||||||
|
- [ ] Device discovery (mDNS when available)
|
||||||
|
- [ ] Web UI for board settings
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
"""Olimex ESP32-C6-EVB Integration for Home Assistant."""
|
||||||
|
import logging
|
||||||
|
import aiohttp
|
||||||
|
from homeassistant.config_entries import ConfigEntry, SOURCE_IMPORT
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.const import CONF_HOST, CONF_PORT, Platform
|
||||||
|
from homeassistant.components.webhook import (
|
||||||
|
async_register as webhook_register,
|
||||||
|
async_unregister as webhook_unregister,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .const import DOMAIN, CONF_CALLBACK_IP, DEFAULT_CALLBACK_IP
|
||||||
|
from .webhook import handle_input_event
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SWITCH]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup(hass: HomeAssistant, config: dict) -> bool:
|
||||||
|
"""Set up the Olimex ESP32-C6-EVB component."""
|
||||||
|
hass.data.setdefault(DOMAIN, {})
|
||||||
|
|
||||||
|
# Handle YAML configuration
|
||||||
|
if DOMAIN in config:
|
||||||
|
for device_config in config[DOMAIN]:
|
||||||
|
hass.async_create_task(
|
||||||
|
hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_IMPORT},
|
||||||
|
data=device_config,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
_LOGGER.debug("Olimex ESP32-C6-EVB integration initialized")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Set up Olimex ESP32-C6-EVB from a config entry."""
|
||||||
|
_LOGGER.info("Setting up Olimex ESP32-C6-EVB entry: %s", entry.entry_id)
|
||||||
|
|
||||||
|
host = entry.data.get(CONF_HOST, "192.168.0.181")
|
||||||
|
port = entry.data.get(CONF_PORT, 80)
|
||||||
|
callback_ip = entry.data.get(CONF_CALLBACK_IP, DEFAULT_CALLBACK_IP)
|
||||||
|
|
||||||
|
hass.data.setdefault(DOMAIN, {})
|
||||||
|
hass.data[DOMAIN][entry.entry_id] = {
|
||||||
|
"host": host,
|
||||||
|
"port": port,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Tell the board where to POST input events
|
||||||
|
try:
|
||||||
|
callback_url = f"http://{callback_ip}:8123/api/webhook/{entry.entry_id}"
|
||||||
|
register_url = f"http://{host}:{port}/register?callback_url={callback_url}"
|
||||||
|
_LOGGER.info("Registering webhook with board at %s:%d (callback %s)", host, port, callback_url)
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.post(register_url, timeout=aiohttp.ClientTimeout(total=5)) as response:
|
||||||
|
if response.status == 200:
|
||||||
|
_LOGGER.info("Board webhook registered successfully")
|
||||||
|
else:
|
||||||
|
_LOGGER.warning("Board registration returned status %d", response.status)
|
||||||
|
except Exception as err:
|
||||||
|
_LOGGER.warning("Failed to register webhook with board: %s", err)
|
||||||
|
|
||||||
|
# Register HA webhook handler to receive input events from the board
|
||||||
|
# Unregister first in case a previous failed setup left it registered
|
||||||
|
try:
|
||||||
|
webhook_unregister(hass, entry.entry_id)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
webhook_register(
|
||||||
|
hass,
|
||||||
|
DOMAIN,
|
||||||
|
"Olimex Input Event",
|
||||||
|
entry.entry_id,
|
||||||
|
handle_input_event,
|
||||||
|
)
|
||||||
|
_LOGGER.info("HA webhook handler registered for entry %s", entry.entry_id)
|
||||||
|
|
||||||
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
_LOGGER.info("Olimex ESP32-C6-EVB configured for %s:%d", host, port)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||||
|
"""Clean up when integration is fully removed (called after unload)."""
|
||||||
|
_LOGGER.info("Removing Olimex ESP32-C6-EVB entry: %s", entry.entry_id)
|
||||||
|
# Remove any leftover domain data bucket if it's now empty
|
||||||
|
if DOMAIN in hass.data and not hass.data[DOMAIN]:
|
||||||
|
hass.data.pop(DOMAIN)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Unload a config entry."""
|
||||||
|
try:
|
||||||
|
_LOGGER.info("Unloading Olimex ESP32-C6-EVB entry: %s", entry.entry_id)
|
||||||
|
|
||||||
|
# Unregister webhook handler
|
||||||
|
webhook_unregister(hass, entry.entry_id)
|
||||||
|
|
||||||
|
# Unload all platforms
|
||||||
|
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
|
|
||||||
|
if unload_ok:
|
||||||
|
# Clean up data
|
||||||
|
if entry.entry_id in hass.data.get(DOMAIN, {}):
|
||||||
|
hass.data[DOMAIN].pop(entry.entry_id)
|
||||||
|
_LOGGER.debug("Cleaned up data for entry %s", entry.entry_id)
|
||||||
|
|
||||||
|
_LOGGER.info("Successfully unloaded Olimex ESP32-C6-EVB entry: %s", entry.entry_id)
|
||||||
|
return unload_ok
|
||||||
|
except Exception as err:
|
||||||
|
_LOGGER.error("Error unloading Olimex ESP32-C6-EVB entry: %s", err, exc_info=True)
|
||||||
|
return False
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
"""Binary sensor platform for Olimex ESP32-C6-EVB."""
|
||||||
|
import logging
|
||||||
|
from homeassistant.components.binary_sensor import BinarySensorEntity
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
|
|
||||||
|
from .const import DOMAIN, NUM_INPUTS
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up binary sensor entities for inputs."""
|
||||||
|
data = hass.data[DOMAIN][entry.entry_id]
|
||||||
|
|
||||||
|
sensors = [
|
||||||
|
OlimexInputSensor(hass, entry, input_num)
|
||||||
|
for input_num in range(1, NUM_INPUTS + 1)
|
||||||
|
]
|
||||||
|
async_add_entities(sensors, update_before_add=False)
|
||||||
|
|
||||||
|
|
||||||
|
class OlimexInputSensor(BinarySensorEntity):
|
||||||
|
"""Binary sensor for Olimex input pin.
|
||||||
|
|
||||||
|
State is driven exclusively by webhook POSTs from the board.
|
||||||
|
No polling is performed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_attr_should_poll = False
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant, entry: ConfigEntry, input_num: int):
|
||||||
|
"""Initialize the sensor."""
|
||||||
|
self.hass = hass
|
||||||
|
self._entry = entry
|
||||||
|
self._input_num = input_num
|
||||||
|
self._attr_name = f"Input {input_num}"
|
||||||
|
self._attr_unique_id = f"{entry.entry_id}_input_{input_num}"
|
||||||
|
self._state = False
|
||||||
|
|
||||||
|
async def async_added_to_hass(self):
|
||||||
|
"""Subscribe to webhook dispatcher when entity is added."""
|
||||||
|
signal = f"{DOMAIN}_input_{self._input_num}_event"
|
||||||
|
self.async_on_remove(
|
||||||
|
async_dispatcher_connect(self.hass, signal, self._handle_webhook_event)
|
||||||
|
)
|
||||||
|
await super().async_added_to_hass()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _handle_webhook_event(self, state):
|
||||||
|
"""Handle real-time input event received via webhook from the board."""
|
||||||
|
# Board already inverts pull-up logic before sending:
|
||||||
|
# state=True means pressed, state=False means released
|
||||||
|
self._state = state
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Input %d webhook event: state=%s (sensor is_on=%s)",
|
||||||
|
self._input_num, state, self._state
|
||||||
|
)
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self):
|
||||||
|
"""Return True if input is pressed."""
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self):
|
||||||
|
"""Return device information."""
|
||||||
|
try:
|
||||||
|
host = self._entry.data.get('host', 'unknown')
|
||||||
|
return {
|
||||||
|
"identifiers": {(DOMAIN, self._entry.entry_id)},
|
||||||
|
"name": f"Olimex ESP32-C6 ({host})",
|
||||||
|
"manufacturer": "Olimex",
|
||||||
|
"model": "ESP32-C6-EVB",
|
||||||
|
}
|
||||||
|
except Exception as err:
|
||||||
|
_LOGGER.debug("Error getting device info: %s", err)
|
||||||
|
return {
|
||||||
|
"identifiers": {(DOMAIN, self._entry.entry_id)},
|
||||||
|
"name": "Olimex ESP32-C6",
|
||||||
|
"manufacturer": "Olimex",
|
||||||
|
"model": "ESP32-C6-EVB",
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
"""Config flow for Olimex ESP32-C6-EVB integration."""
|
||||||
|
import logging
|
||||||
|
import voluptuous as vol
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||||
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
|
||||||
|
from .const import DOMAIN, DEFAULT_PORT, CONF_CALLBACK_IP, DEFAULT_CALLBACK_IP
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class OlimexESP32C6ConfigFlow(config_entries.ConfigFlow, domain="olimex_esp32_c6"):
|
||||||
|
"""Handle a config flow for Olimex ESP32-C6-EVB."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
|
||||||
|
async def async_step_user(self, user_input=None):
|
||||||
|
"""Handle the initial step."""
|
||||||
|
errors = {}
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
host = user_input.get(CONF_HOST, "192.168.0.181")
|
||||||
|
port = user_input.get(CONF_PORT, DEFAULT_PORT)
|
||||||
|
|
||||||
|
# Validate connection to the board
|
||||||
|
try:
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
url = f"http://{host}:{port}/api/status"
|
||||||
|
async with session.get(
|
||||||
|
url,
|
||||||
|
timeout=aiohttp.ClientTimeout(total=10)
|
||||||
|
) as resp:
|
||||||
|
if resp.status != 200:
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
|
else:
|
||||||
|
await self.async_set_unique_id(f"{host}:{port}")
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
|
_LOGGER.info("Successfully connected to Olimex ESP32-C6 at %s", host)
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=f"Olimex ESP32-C6 ({host})",
|
||||||
|
data={
|
||||||
|
CONF_HOST: host,
|
||||||
|
CONF_PORT: port,
|
||||||
|
CONF_CALLBACK_IP: user_input.get(
|
||||||
|
CONF_CALLBACK_IP, DEFAULT_CALLBACK_IP
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
except aiohttp.ClientError:
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
|
_LOGGER.error("Failed to connect to Olimex ESP32-C6 at %s", host)
|
||||||
|
except Exception as err:
|
||||||
|
errors["base"] = "unknown"
|
||||||
|
_LOGGER.error("Error in config flow: %s", err)
|
||||||
|
|
||||||
|
data_schema = vol.Schema({
|
||||||
|
vol.Required(CONF_HOST, default="192.168.0.181"): str,
|
||||||
|
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||||
|
vol.Optional(CONF_CALLBACK_IP, default=DEFAULT_CALLBACK_IP): str,
|
||||||
|
})
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user",
|
||||||
|
data_schema=data_schema,
|
||||||
|
errors=errors,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_import(self, import_data):
|
||||||
|
"""Handle import from YAML configuration."""
|
||||||
|
_LOGGER.debug("Importing Olimex ESP32-C6 from YAML: %s", import_data)
|
||||||
|
host = import_data.get(CONF_HOST, "192.168.0.181")
|
||||||
|
port = import_data.get(CONF_PORT, DEFAULT_PORT)
|
||||||
|
|
||||||
|
# Check if already configured
|
||||||
|
await self.async_set_unique_id(f"{host}:{port}")
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
|
# Validate connection
|
||||||
|
try:
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
url = f"http://{host}:{port}/api/status"
|
||||||
|
async with session.get(
|
||||||
|
url,
|
||||||
|
timeout=aiohttp.ClientTimeout(total=10)
|
||||||
|
) as resp:
|
||||||
|
if resp.status == 200:
|
||||||
|
_LOGGER.info("Successfully imported Olimex ESP32-C6 from YAML at %s", host)
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=f"Olimex ESP32-C6 ({host})",
|
||||||
|
data={
|
||||||
|
CONF_HOST: host,
|
||||||
|
CONF_PORT: port,
|
||||||
|
CONF_CALLBACK_IP: import_data.get(CONF_CALLBACK_IP, DEFAULT_CALLBACK_IP),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
except Exception as err:
|
||||||
|
_LOGGER.error("Failed to import Olimex ESP32-C6 from YAML: %s", err)
|
||||||
|
|
||||||
|
# If validation fails, still create entry but log warning
|
||||||
|
_LOGGER.warning("Could not validate Olimex ESP32-C6 at %s, creating entry anyway", host)
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=f"Olimex ESP32-C6 ({host})",
|
||||||
|
data={
|
||||||
|
CONF_HOST: host,
|
||||||
|
CONF_PORT: port,
|
||||||
|
CONF_CALLBACK_IP: import_data.get(CONF_CALLBACK_IP, DEFAULT_CALLBACK_IP),
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
"""Constants for the Olimex ESP32-C6-EVB integration."""
|
||||||
|
|
||||||
|
DOMAIN = "olimex_esp32_c6"
|
||||||
|
MANUFACTURER = "Olimex"
|
||||||
|
MODEL = "ESP32-C6-EVB"
|
||||||
|
CHIP = "ESP32-C6 WROOM-1"
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
CONF_HOST = "host"
|
||||||
|
CONF_PORT = "port"
|
||||||
|
CONF_SCAN_INTERVAL = "scan_interval"
|
||||||
|
CONF_CALLBACK_IP = "callback_ip"
|
||||||
|
|
||||||
|
# Default values
|
||||||
|
DEFAULT_PORT = 80
|
||||||
|
DEFAULT_SCAN_INTERVAL = 5
|
||||||
|
DEFAULT_CALLBACK_IP = "192.168.0.1"
|
||||||
|
|
||||||
|
# Relay and Input info
|
||||||
|
NUM_RELAYS = 4
|
||||||
|
NUM_INPUTS = 4
|
||||||
|
|
||||||
|
# Device info
|
||||||
|
DEVICE_INFO = {
|
||||||
|
"manufacturer": MANUFACTURER,
|
||||||
|
"model": MODEL,
|
||||||
|
"chip": CHIP,
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"domain": "olimex_esp32_c6",
|
||||||
|
"name": "Olimex ESP32-C6-EVB",
|
||||||
|
"codeowners": [],
|
||||||
|
"config_flow": true,
|
||||||
|
"dependencies": [],
|
||||||
|
"documentation": "https://github.com/yourusername/olimex-esp32-c6-ha",
|
||||||
|
"iot_class": "local_polling",
|
||||||
|
"requirements": ["aiohttp>=3.8.0"],
|
||||||
|
"version": "0.1.0"
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 54 KiB |
@@ -0,0 +1,122 @@
|
|||||||
|
"""Sensor platform for Olimex ESP32-C6-EVB."""
|
||||||
|
import logging
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
from homeassistant.components.sensor import SensorEntity
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.helpers.update_coordinator import (
|
||||||
|
CoordinatorEntity,
|
||||||
|
DataUpdateCoordinator,
|
||||||
|
UpdateFailed,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .const import DOMAIN, DEVICE_INFO, DEFAULT_SCAN_INTERVAL
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up Olimex ESP32-C6-EVB sensors."""
|
||||||
|
host = entry.data["host"]
|
||||||
|
port = entry.data.get("port", 80)
|
||||||
|
|
||||||
|
coordinator = OlimexDataUpdateCoordinator(hass, host, port)
|
||||||
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
|
sensors = [
|
||||||
|
OlimexTemperatureSensor(coordinator, entry),
|
||||||
|
OlimexWiFiSignalSensor(coordinator, entry),
|
||||||
|
]
|
||||||
|
|
||||||
|
async_add_entities(sensors)
|
||||||
|
|
||||||
|
class OlimexDataUpdateCoordinator(DataUpdateCoordinator):
|
||||||
|
"""Data coordinator for Olimex ESP32-C6-EVB."""
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant, host: str, port: int) -> None:
|
||||||
|
"""Initialize coordinator."""
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
_LOGGER,
|
||||||
|
name=DOMAIN,
|
||||||
|
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
|
||||||
|
)
|
||||||
|
self.host = host
|
||||||
|
self.port = port
|
||||||
|
|
||||||
|
async def _async_update_data(self):
|
||||||
|
"""Fetch data from ESP32-C6."""
|
||||||
|
try:
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
# TODO: Update this URL based on your ESP32 firmware API
|
||||||
|
async with session.get(
|
||||||
|
f"http://{self.host}:{self.port}/api/status",
|
||||||
|
timeout=aiohttp.ClientTimeout(total=10),
|
||||||
|
) as response:
|
||||||
|
if response.status == 200:
|
||||||
|
return await response.json()
|
||||||
|
raise UpdateFailed(f"Error fetching data: {response.status}")
|
||||||
|
except aiohttp.ClientError as err:
|
||||||
|
raise UpdateFailed(f"Error communicating with device: {err}")
|
||||||
|
|
||||||
|
class OlimexTemperatureSensor(CoordinatorEntity, SensorEntity):
|
||||||
|
"""Temperature sensor for ESP32-C6."""
|
||||||
|
|
||||||
|
def __init__(self, coordinator, entry):
|
||||||
|
"""Initialize the sensor."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
self._entry = entry
|
||||||
|
self._attr_name = "Temperature"
|
||||||
|
self._attr_unique_id = f"{entry.entry_id}_temperature"
|
||||||
|
self._attr_native_unit_of_measurement = "°C"
|
||||||
|
self._attr_device_class = "temperature"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self):
|
||||||
|
"""Return device information."""
|
||||||
|
return {
|
||||||
|
"identifiers": {(DOMAIN, self._entry.entry_id)},
|
||||||
|
"name": f"Olimex ESP32-C6 ({self._entry.data['host']})",
|
||||||
|
**DEVICE_INFO,
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_value(self):
|
||||||
|
"""Return the temperature value."""
|
||||||
|
if self.coordinator.data:
|
||||||
|
return self.coordinator.data.get("temperature")
|
||||||
|
return None
|
||||||
|
|
||||||
|
class OlimexWiFiSignalSensor(CoordinatorEntity, SensorEntity):
|
||||||
|
"""WiFi signal sensor for ESP32-C6."""
|
||||||
|
|
||||||
|
def __init__(self, coordinator, entry):
|
||||||
|
"""Initialize the sensor."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
self._entry = entry
|
||||||
|
self._attr_name = "WiFi Signal"
|
||||||
|
self._attr_unique_id = f"{entry.entry_id}_wifi_signal"
|
||||||
|
self._attr_native_unit_of_measurement = "dBm"
|
||||||
|
self._attr_device_class = "signal_strength"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self):
|
||||||
|
"""Return device information."""
|
||||||
|
return {
|
||||||
|
"identifiers": {(DOMAIN, self._entry.entry_id)},
|
||||||
|
"name": f"Olimex ESP32-C6 ({self._entry.data['host']})",
|
||||||
|
**DEVICE_INFO,
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_value(self):
|
||||||
|
"""Return the WiFi signal strength."""
|
||||||
|
if self.coordinator.data:
|
||||||
|
return self.coordinator.data.get("wifi_rssi")
|
||||||
|
return None
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
"""Relay status sensor updater for Olimex ESP32-C6-EVB."""
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
from homeassistant.helpers.update_coordinator import (
|
||||||
|
CoordinatorEntity,
|
||||||
|
DataUpdateCoordinator,
|
||||||
|
UpdateFailed,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.components.sensor import SensorEntity
|
||||||
|
|
||||||
|
from .const import DOMAIN, DEFAULT_SCAN_INTERVAL
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class OlimexDataUpdateCoordinator(DataUpdateCoordinator):
|
||||||
|
"""Coordinator to fetch relay statuses from the board."""
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant, host: str, port: int, scan_interval: int) -> None:
|
||||||
|
"""Initialize the coordinator."""
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
_LOGGER,
|
||||||
|
name=DOMAIN,
|
||||||
|
update_interval=timedelta(seconds=scan_interval),
|
||||||
|
)
|
||||||
|
self.host = host
|
||||||
|
self.port = port
|
||||||
|
|
||||||
|
async def _fetch_single_relay(self, session: aiohttp.ClientSession, relay_num: int):
|
||||||
|
"""Fetch one relay's status, returning (relay_num, state)."""
|
||||||
|
try:
|
||||||
|
url = f"http://{self.host}:{self.port}/relay/status?relay={relay_num}"
|
||||||
|
async with session.get(url, timeout=aiohttp.ClientTimeout(total=5)) as response:
|
||||||
|
if response.status == 200:
|
||||||
|
data = await response.json()
|
||||||
|
return relay_num, data.get("state", False)
|
||||||
|
return relay_num, False
|
||||||
|
except Exception as err:
|
||||||
|
_LOGGER.debug("Error fetching relay %d status: %s", relay_num, err)
|
||||||
|
return relay_num, False
|
||||||
|
|
||||||
|
async def _async_update_data(self):
|
||||||
|
"""Fetch all relay statuses from the device in parallel."""
|
||||||
|
try:
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
results = await asyncio.gather(
|
||||||
|
*[self._fetch_single_relay(session, n) for n in range(1, 5)]
|
||||||
|
)
|
||||||
|
return {f"relay_{num}": state for num, state in results}
|
||||||
|
except Exception as err:
|
||||||
|
raise UpdateFailed(f"Error communicating with device: {err}")
|
||||||
|
|
||||||
|
|
||||||
|
class RelayStatusSensor(CoordinatorEntity, SensorEntity):
|
||||||
|
"""Sensor for relay status."""
|
||||||
|
|
||||||
|
def __init__(self, coordinator, entry, relay_num):
|
||||||
|
"""Initialize the sensor."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
self._entry = entry
|
||||||
|
self._relay_num = relay_num
|
||||||
|
self._attr_name = f"Relay {relay_num} Status"
|
||||||
|
self._attr_unique_id = f"{entry.entry_id}_relay_{relay_num}_status"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_value(self):
|
||||||
|
"""Return the relay status."""
|
||||||
|
if self.coordinator.data:
|
||||||
|
return self.coordinator.data.get(f"relay_{self._relay_num}", False)
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self):
|
||||||
|
"""Return device information."""
|
||||||
|
return {
|
||||||
|
"identifiers": {(DOMAIN, self._entry.entry_id)},
|
||||||
|
"name": f"Olimex ESP32-C6 ({self._entry.data['host']})",
|
||||||
|
"manufacturer": "Olimex",
|
||||||
|
"model": "ESP32-C6-EVB",
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"title": "Configure Olimex ESP32-C6-EVB",
|
||||||
|
"description": "Enter the network details for your Olimex ESP32-C6-EVB board",
|
||||||
|
"data": {
|
||||||
|
"host": "Device IP Address or Hostname",
|
||||||
|
"port": "Port",
|
||||||
|
"callback_ip": "Home Assistant IP (for board → HA webhook)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "Failed to connect to the device. Please check the IP address and ensure the board is online.",
|
||||||
|
"invalid_auth": "Authentication failed",
|
||||||
|
"unknown": "An unexpected error occurred"
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Device is already configured"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"title": "Olimex ESP32-C6-EVB Options",
|
||||||
|
"data": {
|
||||||
|
"callback_ip": "Home Assistant IP (for board → HA webhook)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"entity": {
|
||||||
|
"binary_sensor": {
|
||||||
|
"input_1": {
|
||||||
|
"name": "Input 1"
|
||||||
|
},
|
||||||
|
"input_2": {
|
||||||
|
"name": "Input 2"
|
||||||
|
},
|
||||||
|
"input_3": {
|
||||||
|
"name": "Input 3"
|
||||||
|
},
|
||||||
|
"input_4": {
|
||||||
|
"name": "Input 4"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
"""Switch platform for Olimex ESP32-C6-EVB."""
|
||||||
|
import logging
|
||||||
|
import aiohttp
|
||||||
|
from homeassistant.components.switch import SwitchEntity
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
|
from .const import DOMAIN, NUM_RELAYS
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Tight timeout for a local LAN device
|
||||||
|
_TIMEOUT = aiohttp.ClientTimeout(total=3)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up switch entities for relays."""
|
||||||
|
data = hass.data[DOMAIN][entry.entry_id]
|
||||||
|
host = data["host"]
|
||||||
|
port = data["port"]
|
||||||
|
|
||||||
|
switches = [
|
||||||
|
OlimexRelaySwitch(entry, host, port, relay_num)
|
||||||
|
for relay_num in range(1, NUM_RELAYS + 1)
|
||||||
|
]
|
||||||
|
async_add_entities(switches, update_before_add=False)
|
||||||
|
|
||||||
|
|
||||||
|
class OlimexRelaySwitch(SwitchEntity):
|
||||||
|
"""Switch for Olimex relay.
|
||||||
|
|
||||||
|
State is set on load via a single GET and on every toggle via the
|
||||||
|
state value returned directly in the POST response — no extra round-trip.
|
||||||
|
A single persistent aiohttp session is reused for all requests.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_attr_should_poll = False
|
||||||
|
|
||||||
|
def __init__(self, entry: ConfigEntry, host: str, port: int, relay_num: int):
|
||||||
|
"""Initialize the switch."""
|
||||||
|
self._entry = entry
|
||||||
|
self._host = host
|
||||||
|
self._port = port
|
||||||
|
self._relay_num = relay_num
|
||||||
|
self._attr_name = f"Relay {relay_num}"
|
||||||
|
self._attr_unique_id = f"{entry.entry_id}_relay_{relay_num}"
|
||||||
|
self._is_on = False
|
||||||
|
self._session: aiohttp.ClientSession | None = None
|
||||||
|
|
||||||
|
async def async_added_to_hass(self):
|
||||||
|
"""Open a persistent HTTP session and fetch initial relay state."""
|
||||||
|
self._session = aiohttp.ClientSession()
|
||||||
|
url = f"http://{self._host}:{self._port}/relay/status?relay={self._relay_num}"
|
||||||
|
try:
|
||||||
|
async with self._session.get(url, timeout=_TIMEOUT) as resp:
|
||||||
|
if resp.status == 200:
|
||||||
|
data = await resp.json()
|
||||||
|
self._is_on = data.get("state", False)
|
||||||
|
except Exception as err:
|
||||||
|
_LOGGER.debug("Relay %d initial fetch failed: %s", self._relay_num, err)
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
async def async_will_remove_from_hass(self):
|
||||||
|
"""Close the HTTP session when entity is removed."""
|
||||||
|
if self._session:
|
||||||
|
await self._session.close()
|
||||||
|
self._session = None
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# SwitchEntity interface
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
async def async_turn_on(self, **kwargs):
|
||||||
|
"""Turn the relay on."""
|
||||||
|
await self._async_set_relay(True)
|
||||||
|
|
||||||
|
async def async_turn_off(self, **kwargs):
|
||||||
|
"""Turn the relay off."""
|
||||||
|
await self._async_set_relay(False)
|
||||||
|
|
||||||
|
async def _async_set_relay(self, on: bool):
|
||||||
|
"""POST on/off to the board; read state from the response body directly."""
|
||||||
|
action = "on" if on else "off"
|
||||||
|
url = f"http://{self._host}:{self._port}/relay/{action}?relay={self._relay_num}"
|
||||||
|
try:
|
||||||
|
async with self._session.post(url, timeout=_TIMEOUT) as resp:
|
||||||
|
if resp.status == 200:
|
||||||
|
data = await resp.json()
|
||||||
|
# Board returns {"status":"ok","state":true/false} — use it directly
|
||||||
|
self._is_on = data.get("state", on)
|
||||||
|
_LOGGER.debug("Relay %d -> %s (board confirmed: %s)", self._relay_num, action, self._is_on)
|
||||||
|
else:
|
||||||
|
_LOGGER.error("Relay %d %s failed: HTTP %d", self._relay_num, action, resp.status)
|
||||||
|
except Exception as err:
|
||||||
|
_LOGGER.error("Relay %d %s error: %s", self._relay_num, action, err)
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self):
|
||||||
|
"""Return True if relay is on."""
|
||||||
|
return self._is_on
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self):
|
||||||
|
"""Return device information."""
|
||||||
|
host = self._entry.data.get("host", "unknown")
|
||||||
|
return {
|
||||||
|
"identifiers": {(DOMAIN, self._entry.entry_id)},
|
||||||
|
"name": f"Olimex ESP32-C6 ({host})",
|
||||||
|
"manufacturer": "Olimex",
|
||||||
|
"model": "ESP32-C6-EVB",
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
"""Webhook handler for Olimex ESP32-C6-EVB input events."""
|
||||||
|
import logging
|
||||||
|
from aiohttp import web
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_input_event(hass: HomeAssistant, webhook_id: str, request) -> web.Response:
|
||||||
|
"""Handle input event webhook from the board."""
|
||||||
|
try:
|
||||||
|
data = await request.json()
|
||||||
|
input_num = data.get("input")
|
||||||
|
state = data.get("state")
|
||||||
|
|
||||||
|
_LOGGER.info("Received input event: input=%s state=%s", input_num, state)
|
||||||
|
|
||||||
|
# Dispatch signal to update binary sensors immediately
|
||||||
|
signal = f"{DOMAIN}_input_{input_num}_event"
|
||||||
|
async_dispatcher_send(hass, signal, state)
|
||||||
|
|
||||||
|
return web.json_response({"status": "ok"})
|
||||||
|
|
||||||
|
except Exception as err:
|
||||||
|
_LOGGER.error("Error handling webhook: %s", err)
|
||||||
|
return web.json_response({"error": str(err)}, status=400)
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
# ESP32-C6 Arduino Deployment Guide
|
||||||
|
|
||||||
|
Complete guide to compile and deploy the firmware to your Olimex ESP32-C6-EVB board.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✓ Pre-Deployment Checklist
|
||||||
|
|
||||||
|
- [ ] Arduino IDE installed (version 2.0+)
|
||||||
|
- [ ] ESP32 board package installed (version 3.0.0+)
|
||||||
|
- [ ] Olimex ESP32-C6-EVB board connected via USB
|
||||||
|
- [ ] USB drivers installed for your OS
|
||||||
|
- [ ] WiFi credentials available
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 1: Install Arduino IDE
|
||||||
|
|
||||||
|
1. Download from: https://www.arduino.cc/en/software
|
||||||
|
2. Install and launch Arduino IDE 2.0 or later
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 2: Add ESP32 Board Support
|
||||||
|
|
||||||
|
### Windows/Mac/Linux (same process):
|
||||||
|
|
||||||
|
1. **Open Preferences**
|
||||||
|
- File → Preferences (or Arduino IDE → Settings on Mac)
|
||||||
|
|
||||||
|
2. **Add Board URL**
|
||||||
|
- Find "Additional Boards Manager URLs" field
|
||||||
|
- Add this URL:
|
||||||
|
```
|
||||||
|
https://espressif.github.io/arduino-esp32/package_esp32_index.json
|
||||||
|
```
|
||||||
|
- Click OK
|
||||||
|
|
||||||
|
3. **Install ESP32 Board Package**
|
||||||
|
- Tools → Board → Boards Manager
|
||||||
|
- Search: "esp32"
|
||||||
|
- Install "esp32 by Espressif Systems" (version 3.0.0+)
|
||||||
|
- Wait for installation to complete
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 3: Configure Board Settings
|
||||||
|
|
||||||
|
After installation, configure these exact settings in Arduino IDE:
|
||||||
|
|
||||||
|
**Tools Menu Settings:**
|
||||||
|
|
||||||
|
| Setting | Value |
|
||||||
|
|---------|-------|
|
||||||
|
| Board | ESP32C6 Dev Module |
|
||||||
|
| Upload Speed | 921600 |
|
||||||
|
| USB CDC On Boot | **Enabled** ⚠️ CRITICAL |
|
||||||
|
| Flash Size | 4MB |
|
||||||
|
| Flash Mode | DIO |
|
||||||
|
| Flash Frequency | 80MHz |
|
||||||
|
| Partition Scheme | Default 4MB |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 4: Update WiFi Credentials
|
||||||
|
|
||||||
|
**Before uploading**, edit the WiFi credentials in `esp32_arduino.ino`:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Line 16-17 - CHANGE THESE:
|
||||||
|
const char* ssid = "Your_WiFi_SSID_Here";
|
||||||
|
const char* password = "Your_WiFi_Password_Here";
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace with your actual WiFi network name and password.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 5: Connect Board & Find USB Port
|
||||||
|
|
||||||
|
### Linux/Mac:
|
||||||
|
```bash
|
||||||
|
# Check available ports
|
||||||
|
ls -la /dev/ttyACM* /dev/ttyUSB*
|
||||||
|
|
||||||
|
# Should see something like: /dev/ttyACM0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Windows:
|
||||||
|
- Device Manager → Ports (COM & LPT)
|
||||||
|
- Look for "USB-UART Bridge" or similar
|
||||||
|
|
||||||
|
### Select Port in Arduino IDE:
|
||||||
|
- Tools → Port → [Select your port]
|
||||||
|
- Usually `/dev/ttyACM0` on Linux (not `/dev/ttyUSB0`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 6: Compile & Upload
|
||||||
|
|
||||||
|
1. **Verify Sketch** (check for errors before upload)
|
||||||
|
- Sketch → Verify/Compile
|
||||||
|
- OR press: Ctrl+R (Windows/Linux) or Cmd+R (Mac)
|
||||||
|
|
||||||
|
2. **Upload to Board**
|
||||||
|
- Sketch → Upload
|
||||||
|
- OR press: Ctrl+U (Windows/Linux) or Cmd+U (Mac)
|
||||||
|
|
||||||
|
3. **Watch Serial Monitor**
|
||||||
|
- Tools → Serial Monitor (or Ctrl+Shift+M)
|
||||||
|
- **Set baud rate to: 115200**
|
||||||
|
- You should see the startup messages
|
||||||
|
|
||||||
|
4. **If No Output:**
|
||||||
|
- Press the **RESET button** on the ESP32-C6 board
|
||||||
|
- Check that "USB CDC On Boot" is set to **Enabled**
|
||||||
|
- Verify Serial Monitor baud rate is 115200
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 7: Verify Deployment Success
|
||||||
|
|
||||||
|
### Expected Serial Output:
|
||||||
|
|
||||||
|
```
|
||||||
|
=================================
|
||||||
|
ESP32-C6 Home Assistant Device
|
||||||
|
Arduino Framework
|
||||||
|
=================================
|
||||||
|
GPIO initialized
|
||||||
|
Connecting to WiFi: Your_WiFi_SSID
|
||||||
|
.........................
|
||||||
|
✓ WiFi connected!
|
||||||
|
IP address: 192.168.1.xxx
|
||||||
|
RSSI: -45 dBm
|
||||||
|
MAC: AA:BB:CC:DD:EE:FF
|
||||||
|
✓ HTTP server started on port 80
|
||||||
|
=================================
|
||||||
|
Ready! Try these endpoints:
|
||||||
|
http://192.168.1.xxx/api/status
|
||||||
|
=================================
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test the Board
|
||||||
|
|
||||||
|
**From Linux terminal or browser:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get board status
|
||||||
|
curl http://192.168.1.xxx/api/status
|
||||||
|
|
||||||
|
# Expected response:
|
||||||
|
# {"temperature":25.0,"wifi_rssi":-45,"chip":"ESP32-C6","free_heap":123456,"uptime":12,"ip":"192.168.1.xxx","relay1":false,"led":false}
|
||||||
|
|
||||||
|
# Turn relay ON
|
||||||
|
curl -X POST http://192.168.1.xxx/api/relay/relay_1/on
|
||||||
|
|
||||||
|
# Turn relay OFF
|
||||||
|
curl -X POST http://192.168.1.xxx/api/relay/relay_1/off
|
||||||
|
|
||||||
|
# Get relay status
|
||||||
|
curl http://192.168.1.xxx/api/relay/relay_1/status
|
||||||
|
|
||||||
|
# Turn LED ON
|
||||||
|
curl -X POST http://192.168.1.xxx/api/led/led/on
|
||||||
|
|
||||||
|
# Turn LED OFF
|
||||||
|
curl -X POST http://192.168.1.xxx/api/led/led/off
|
||||||
|
```
|
||||||
|
|
||||||
|
**Or open in browser:**
|
||||||
|
- `http://192.168.1.xxx/` - Web control panel
|
||||||
|
- `http://192.168.1.xxx/api/status` - JSON status
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 8: Add to Home Assistant
|
||||||
|
|
||||||
|
1. **Get the board's IP address** (from Serial Monitor output)
|
||||||
|
|
||||||
|
2. **In Home Assistant:**
|
||||||
|
- Settings → Devices & Services → Integrations
|
||||||
|
- Click "+ Create Integration"
|
||||||
|
- Search for "Olimex ESP32-C6-EVB"
|
||||||
|
- Enter IP address and port (80)
|
||||||
|
- Select which relays/LEDs to include
|
||||||
|
|
||||||
|
3. **You should now see:**
|
||||||
|
- Temperature sensor
|
||||||
|
- WiFi signal strength
|
||||||
|
- Relay switch
|
||||||
|
- LED switch
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### No Serial Output
|
||||||
|
- ✓ Verify "USB CDC On Boot" is **Enabled** in board settings
|
||||||
|
- ✓ Press RESET button on the board
|
||||||
|
- ✓ Check Serial Monitor baud rate (115200)
|
||||||
|
- ✓ Try different USB cable
|
||||||
|
- ✓ Try different USB port
|
||||||
|
|
||||||
|
### WiFi Connection Failed
|
||||||
|
- ✓ Verify SSID and password are correct (check for typos)
|
||||||
|
- ✓ Ensure board is within WiFi range
|
||||||
|
- ✓ Check if WiFi network requires WPA3 (ESP32-C6 may have issues with some WPA3 networks)
|
||||||
|
- ✓ Try 2.4GHz network (not 5GHz)
|
||||||
|
|
||||||
|
### Can't Find Board in Arduino IDE
|
||||||
|
- ✓ Check USB cable is connected
|
||||||
|
- ✓ Verify USB drivers installed
|
||||||
|
- ✓ Try different USB port
|
||||||
|
- ✓ Restart Arduino IDE
|
||||||
|
- ✓ On Linux: `sudo usermod -a -G dialout $USER` then relogin
|
||||||
|
|
||||||
|
### API Endpoints Not Responding
|
||||||
|
- ✓ Verify board has WiFi connection (check Serial Monitor)
|
||||||
|
- ✓ Verify correct IP address
|
||||||
|
- ✓ Check firewall isn't blocking port 80
|
||||||
|
- ✓ Restart the board (press RESET button)
|
||||||
|
|
||||||
|
### Relay/LED Not Working
|
||||||
|
- ✓ Check GPIO pin connections (LED=GPIO8, Relay=GPIO2)
|
||||||
|
- ✓ Verify relay/LED hardware is connected correctly
|
||||||
|
- ✓ Test endpoints in Serial Monitor output
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## GPIO Pin Reference
|
||||||
|
|
||||||
|
| Function | GPIO Pin | Usage |
|
||||||
|
|----------|----------|-------|
|
||||||
|
| LED (Onboard) | GPIO 8 | Blue LED control |
|
||||||
|
| Relay 1 | GPIO 2 | Relay control |
|
||||||
|
|
||||||
|
For additional GPIO pins, modify the code and add more handlers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What the Code Does
|
||||||
|
|
||||||
|
✅ **WiFi Connection** - Connects to your WiFi network
|
||||||
|
✅ **REST API** - Provides HTTP endpoints for control
|
||||||
|
✅ **Web UI** - Control panel at root URL
|
||||||
|
✅ **Relay Control** - On/Off control via GPIO2
|
||||||
|
✅ **LED Control** - On/Off control via GPIO8
|
||||||
|
✅ **Temperature Simulation** - Reads and reports simulated temperature
|
||||||
|
✅ **System Status** - Reports uptime, free memory, WiFi signal strength
|
||||||
|
✅ **Serial Debugging** - All actions logged to Serial Monitor
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. Deploy firmware to board ✓
|
||||||
|
2. Verify board works in Serial Monitor ✓
|
||||||
|
3. Test API endpoints from terminal
|
||||||
|
4. Add to Home Assistant integration
|
||||||
|
5. Create automations using the relay/LED as switches
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## More Resources
|
||||||
|
|
||||||
|
- [Arduino IDE Documentation](https://docs.arduino.cc/software/ide-v2)
|
||||||
|
- [Espressif ESP32-C6 Datasheet](https://www.espressif.com/sites/default/files/documentation/esp32-c6_datasheet_en.pdf)
|
||||||
|
- [Olimex ESP32-C6-EVB Board](https://www.olimex.com/Products/IoT/ESP32-C6-EVB/)
|
||||||
|
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
# ESP32-C6 Arduino Project
|
||||||
|
|
||||||
|
Arduino IDE project for ESP32-C6 Home Assistant integration.
|
||||||
|
|
||||||
|
## Arduino IDE Setup
|
||||||
|
|
||||||
|
### 1. Install ESP32 Board Support
|
||||||
|
|
||||||
|
1. Open Arduino IDE
|
||||||
|
2. Go to **File → Preferences**
|
||||||
|
3. Add this URL to "Additional Boards Manager URLs":
|
||||||
|
```
|
||||||
|
https://espressif.github.io/arduino-esp32/package_esp32_index.json
|
||||||
|
```
|
||||||
|
4. Go to **Tools → Board → Boards Manager**
|
||||||
|
5. Search for "esp32" by Espressif Systems
|
||||||
|
6. Install **esp32** (version 3.0.0 or later for ESP32-C6 support)
|
||||||
|
|
||||||
|
### 2. Board Configuration
|
||||||
|
|
||||||
|
In Arduino IDE, select:
|
||||||
|
- **Board**: "ESP32C6 Dev Module"
|
||||||
|
- **Upload Speed**: 921600
|
||||||
|
- **USB CDC On Boot**: **Enabled** ⚠️ **CRITICAL for serial output!**
|
||||||
|
- **Flash Size**: 4MB
|
||||||
|
- **Flash Mode**: DIO
|
||||||
|
- **Flash Frequency**: 80MHz
|
||||||
|
- **Partition Scheme**: Default 4MB
|
||||||
|
- **Port**: `/dev/ttyACM0` (ESP32-C6 typically uses ACM, not USB)
|
||||||
|
|
||||||
|
### 3. Open Project
|
||||||
|
|
||||||
|
1. Open **esp32_arduino.ino** in Arduino IDE
|
||||||
|
2. The IDE will create a folder with the same name automatically
|
||||||
|
|
||||||
|
### 4. Upload
|
||||||
|
|
||||||
|
1. Connect your ESP32-C6 board via USB
|
||||||
|
2. Check available ports: `ls -la /dev/ttyACM* /dev/ttyUSB*`
|
||||||
|
3. Select the correct **Port** in Tools menu (usually `/dev/ttyACM0`)
|
||||||
|
4. Click **Upload** button (→)
|
||||||
|
5. Wait for compilation and upload
|
||||||
|
6. Open **Serial Monitor** (Ctrl+Shift+M) and set to **115200 baud**
|
||||||
|
7. **Press the RESET button** on your board to see output
|
||||||
|
|
||||||
|
**Important**: If you see no serial output:
|
||||||
|
- Verify **USB CDC On Boot** is set to **Enabled**
|
||||||
|
- Press the physical RESET button on the board
|
||||||
|
- Make sure Serial Monitor is set to 115200 baud
|
||||||
|
|
||||||
|
## WiFi Configuration
|
||||||
|
|
||||||
|
WiFi credentials are already set in the code:
|
||||||
|
```cpp
|
||||||
|
const char* ssid = "Buon-Gusto_Nou";
|
||||||
|
const char* password = "arleta13";
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Web Interface
|
||||||
|
After upload, open your browser to the IP shown in Serial Monitor:
|
||||||
|
- Control panel with buttons
|
||||||
|
- Real-time status
|
||||||
|
- API documentation
|
||||||
|
|
||||||
|
### REST API Endpoints
|
||||||
|
|
||||||
|
#### Get Device Status
|
||||||
|
```bash
|
||||||
|
curl http://<ESP32_IP>/api/status
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"temperature": 25.3,
|
||||||
|
"wifi_rssi": -45,
|
||||||
|
"chip": "ESP32-C6",
|
||||||
|
"free_heap": 280000,
|
||||||
|
"uptime": 123,
|
||||||
|
"ip": "192.168.1.100",
|
||||||
|
"relay1": false,
|
||||||
|
"led": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Control Relay
|
||||||
|
```bash
|
||||||
|
# Turn ON
|
||||||
|
curl -X POST http://<ESP32_IP>/api/relay/relay_1/on
|
||||||
|
|
||||||
|
# Turn OFF
|
||||||
|
curl -X POST http://<ESP32_IP>/api/relay/relay_1/off
|
||||||
|
|
||||||
|
# Get Status
|
||||||
|
curl http://<ESP32_IP>/api/relay/relay_1/status
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Control LED
|
||||||
|
```bash
|
||||||
|
# Turn ON
|
||||||
|
curl -X POST http://<ESP32_IP>/api/led/led/on
|
||||||
|
|
||||||
|
# Turn OFF
|
||||||
|
curl -X POST http://<ESP32_IP>/api/led/led/off
|
||||||
|
```
|
||||||
|
|
||||||
|
## GPIO Pins
|
||||||
|
|
||||||
|
- **LED_PIN**: GPIO 8 (onboard LED)
|
||||||
|
- **RELAY_1_PIN**: GPIO 2 (relay control)
|
||||||
|
|
||||||
|
Modify these in the code if your board uses different pins.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Board Not Found in Arduino IDE
|
||||||
|
- Make sure you installed ESP32 board support (minimum version 3.0.0)
|
||||||
|
- Restart Arduino IDE after installation
|
||||||
|
|
||||||
|
### Upload Fails
|
||||||
|
- Check USB cable connection
|
||||||
|
- Select correct port in Tools → Port
|
||||||
|
- Try pressing BOOT button during upload
|
||||||
|
- Reduce upload speed to 115200
|
||||||
|
|
||||||
|
### WiFi Connection Fails
|
||||||
|
- Verify SSID and password
|
||||||
|
- Check if WiFi is 2.4GHz (ESP32-C6 doesn't support 5GHz)
|
||||||
|
- Check Serial Monitor for connection messages
|
||||||
|
|
||||||
|
### Can't See Serial Output
|
||||||
|
- Set Serial Monitor baud rate to **115200**
|
||||||
|
- Enable "USB CDC On Boot" in board settings
|
||||||
|
- Some boards need GPIO 0 held LOW during boot to enter programming mode
|
||||||
|
|
||||||
|
## Serial Monitor Output
|
||||||
|
|
||||||
|
Expected output after successful upload:
|
||||||
|
```
|
||||||
|
=================================
|
||||||
|
ESP32-C6 Home Assistant Device
|
||||||
|
=================================
|
||||||
|
GPIO initialized
|
||||||
|
Connecting to WiFi: Buon-Gusto_Nou
|
||||||
|
..........
|
||||||
|
✓ WiFi connected!
|
||||||
|
IP address: 192.168.1.100
|
||||||
|
RSSI: -45 dBm
|
||||||
|
MAC: AA:BB:CC:DD:EE:FF
|
||||||
|
|
||||||
|
✓ HTTP server started on port 80
|
||||||
|
|
||||||
|
=================================
|
||||||
|
Ready! Try these endpoints:
|
||||||
|
http://192.168.1.100/api/status
|
||||||
|
=================================
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. Upload the code to your ESP32-C6
|
||||||
|
2. Note the IP address from Serial Monitor
|
||||||
|
3. Test the web interface in your browser
|
||||||
|
4. Integrate with Home Assistant using the custom component at `../custom_components/olimex_esp32_c6/`
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": "../../../location_managemet"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../.."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
// ── Board secrets (EXAMPLE — copy to secrets.h and fill in your values) ───────
|
||||||
|
// DO NOT commit secrets.h to version control.
|
||||||
|
//
|
||||||
|
// API_SECRET — shared secret for HMAC-SHA256 API request authentication.
|
||||||
|
// Generate a strong random value with:
|
||||||
|
// python3 -c "import secrets; print(secrets.token_hex(32))"
|
||||||
|
// Then paste the same value into the board's Edit page in the Location
|
||||||
|
// Management server.
|
||||||
|
//
|
||||||
|
// WEB_USER / WEB_PASSWORD — credentials for the browser control panel.
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#define API_SECRET "REPLACE_WITH_OUTPUT_OF_python3_-c_import_secrets_token_hex_32"
|
||||||
|
#define WEB_USER "your_username"
|
||||||
|
#define WEB_PASSWORD "your_password"
|
||||||
Reference in New Issue
Block a user