Initial commit: ESP32-C5-EVB and ESP32-C6-EVB Arduino firmware + HA custom components

This commit is contained in:
ske087
2026-06-11 00:42:59 +03:00
commit e5fd3645d1
41 changed files with 6637 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
# Python
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
# Home Assistant generated
icon.png
# OS
.DS_Store
Thumbs.db
/esp32_arduino/secrets.h
+29
View File
@@ -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 &#8212; Functional Test</h1>";
h += "<div class='sub'>MAC: " + WiFi.macAddress() + " &nbsp; IP: "
+ WiFi.localIP().toString() + " &nbsp; 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 &nbsp; / &nbsp; "
+ String(fail_count) + " FAILED &nbsp; out of "
+ String(result_count) + " tests";
if (all_ok) h += " &nbsp; &#10003; Board OK";
else h += " &nbsp; &#10007; 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();
}
+252
View File
@@ -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/)
+166
View File
@@ -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"