Initial commit: Olimex ESP32-C6-EVB HA integration + Arduino sketch
This commit is contained in:
13
.gitignore
vendored
Normal file
13
.gitignore
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
.Python
|
||||||
|
|
||||||
|
# Home Assistant generated
|
||||||
|
icon.png
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
29
README.md
Normal file
29
README.md
Normal 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.
|
||||||
107
custom_components/olimex_esp32_c6/README.md
Normal file
107
custom_components/olimex_esp32_c6/README.md
Normal file
@@ -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/)
|
||||||
203
custom_components/olimex_esp32_c6/SETUP.md
Normal file
203
custom_components/olimex_esp32_c6/SETUP.md
Normal file
@@ -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
|
||||||
117
custom_components/olimex_esp32_c6/__init__.py
Normal file
117
custom_components/olimex_esp32_c6/__init__.py
Normal file
@@ -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
|
||||||
90
custom_components/olimex_esp32_c6/binary_sensor.py
Normal file
90
custom_components/olimex_esp32_c6/binary_sensor.py
Normal file
@@ -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",
|
||||||
|
}
|
||||||
112
custom_components/olimex_esp32_c6/config_flow.py
Normal file
112
custom_components/olimex_esp32_c6/config_flow.py
Normal file
@@ -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),
|
||||||
|
},
|
||||||
|
)
|
||||||
28
custom_components/olimex_esp32_c6/const.py
Normal file
28
custom_components/olimex_esp32_c6/const.py
Normal file
@@ -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,
|
||||||
|
}
|
||||||
11
custom_components/olimex_esp32_c6/manifest.json
Normal file
11
custom_components/olimex_esp32_c6/manifest.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
BIN
custom_components/olimex_esp32_c6/olimex.png
Normal file
BIN
custom_components/olimex_esp32_c6/olimex.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 54 KiB |
122
custom_components/olimex_esp32_c6/sensor.py
Normal file
122
custom_components/olimex_esp32_c6/sensor.py
Normal file
@@ -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
|
||||||
85
custom_components/olimex_esp32_c6/sensor_updater.py
Normal file
85
custom_components/olimex_esp32_c6/sensor_updater.py
Normal file
@@ -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",
|
||||||
|
}
|
||||||
49
custom_components/olimex_esp32_c6/strings.json
Normal file
49
custom_components/olimex_esp32_c6/strings.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
117
custom_components/olimex_esp32_c6/switch.py
Normal file
117
custom_components/olimex_esp32_c6/switch.py
Normal file
@@ -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",
|
||||||
|
}
|
||||||
29
custom_components/olimex_esp32_c6/webhook.py
Normal file
29
custom_components/olimex_esp32_c6/webhook.py
Normal file
@@ -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)
|
||||||
270
esp32_arduino/DEPLOYMENT_GUIDE.md
Normal file
270
esp32_arduino/DEPLOYMENT_GUIDE.md
Normal file
@@ -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
esp32_arduino/README.md
Normal file
166
esp32_arduino/README.md
Normal 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/`
|
||||||
630
esp32_arduino/esp32_arduino.ino
Normal file
630
esp32_arduino/esp32_arduino.ino
Normal file
@@ -0,0 +1,630 @@
|
|||||||
|
/**
|
||||||
|
* ESP32-C6 Home Assistant Integration
|
||||||
|
* Arduino IDE Project
|
||||||
|
*
|
||||||
|
* Board: ESP32C6 Dev Module
|
||||||
|
* Flash Size: 4MB
|
||||||
|
* USB CDC On Boot: Enabled (REQUIRED for serial output!)
|
||||||
|
*
|
||||||
|
* Provides REST API for Home Assistant integration
|
||||||
|
*/
|
||||||
|
// version 1.5 Initial release
|
||||||
|
#include <WiFi.h>
|
||||||
|
#include <WebServer.h>
|
||||||
|
#include <WiFiClient.h>
|
||||||
|
|
||||||
|
// WiFi credentials
|
||||||
|
const char* ssid = "BUON GUSTO PARTER";
|
||||||
|
const char* password = "arleta13";
|
||||||
|
|
||||||
|
// Web server on port 80
|
||||||
|
WebServer server(80);
|
||||||
|
|
||||||
|
// GPIO pins - Olimex ESP32-C6-EVB board configuration
|
||||||
|
const int LED_PIN = 8; // Onboard LED
|
||||||
|
const int BUT_PIN = 9; // Onboard button
|
||||||
|
const int RELAY_1_PIN = 10; // Relay 1
|
||||||
|
const int RELAY_2_PIN = 11; // Relay 2
|
||||||
|
const int RELAY_3_PIN = 22; // Relay 3
|
||||||
|
const int RELAY_4_PIN = 23; // Relay 4
|
||||||
|
const int DIN1_PIN = 1; // Digital Input 1
|
||||||
|
const int DIN2_PIN = 2; // Digital Input 2
|
||||||
|
const int DIN3_PIN = 3; // Digital Input 3
|
||||||
|
const int DIN4_PIN = 15; // Digital Input 4
|
||||||
|
|
||||||
|
// State tracking
|
||||||
|
bool relay1_state = false;
|
||||||
|
bool relay2_state = false;
|
||||||
|
bool relay3_state = false;
|
||||||
|
bool relay4_state = false;
|
||||||
|
bool led_state = false;
|
||||||
|
|
||||||
|
// Input state tracking - for change detection
|
||||||
|
bool input1_state = true; // HIGH when not pressed (pull-up)
|
||||||
|
bool input2_state = true;
|
||||||
|
bool input3_state = true;
|
||||||
|
bool input4_state = true;
|
||||||
|
bool last_input1_state = true;
|
||||||
|
bool last_input2_state = true;
|
||||||
|
bool last_input3_state = true;
|
||||||
|
bool last_input4_state = true;
|
||||||
|
unsigned long last_input_check = 0;
|
||||||
|
|
||||||
|
// Home Assistant callback configuration
|
||||||
|
char ha_callback_url[256] = ""; // URL to POST input events to
|
||||||
|
bool ha_registered = false;
|
||||||
|
|
||||||
|
// Temperature simulation
|
||||||
|
float temperature = 25.0;
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
// Initialize USB CDC serial
|
||||||
|
Serial.begin(115200);
|
||||||
|
delay(2000); // Give time for USB CDC to initialize
|
||||||
|
|
||||||
|
// Wait for serial port to be ready (up to 5 seconds)
|
||||||
|
for (int i = 0; i < 10 && !Serial; i++) {
|
||||||
|
delay(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.println("\n\n=================================");
|
||||||
|
Serial.println("ESP32-C6 Home Assistant Device");
|
||||||
|
Serial.println("Arduino Framework");
|
||||||
|
Serial.println("=================================");
|
||||||
|
|
||||||
|
// Initialize GPIO - Outputs
|
||||||
|
pinMode(LED_PIN, OUTPUT);
|
||||||
|
pinMode(RELAY_1_PIN, OUTPUT);
|
||||||
|
pinMode(RELAY_2_PIN, OUTPUT);
|
||||||
|
pinMode(RELAY_3_PIN, OUTPUT);
|
||||||
|
pinMode(RELAY_4_PIN, OUTPUT);
|
||||||
|
// Initialize GPIO - Inputs with pull-up
|
||||||
|
pinMode(BUT_PIN, INPUT_PULLUP);
|
||||||
|
pinMode(DIN1_PIN, INPUT_PULLUP);
|
||||||
|
pinMode(DIN2_PIN, INPUT_PULLUP);
|
||||||
|
pinMode(DIN3_PIN, INPUT_PULLUP);
|
||||||
|
pinMode(DIN4_PIN, INPUT_PULLUP);
|
||||||
|
// Set all outputs to LOW
|
||||||
|
digitalWrite(LED_PIN, LOW);
|
||||||
|
digitalWrite(RELAY_1_PIN, LOW);
|
||||||
|
digitalWrite(RELAY_2_PIN, LOW);
|
||||||
|
digitalWrite(RELAY_3_PIN, LOW);
|
||||||
|
digitalWrite(RELAY_4_PIN, LOW);
|
||||||
|
|
||||||
|
Serial.println("GPIO initialized");
|
||||||
|
|
||||||
|
// Configure WiFi
|
||||||
|
Serial.println("\n--- WiFi Configuration ---");
|
||||||
|
WiFi.mode(WIFI_STA);
|
||||||
|
WiFi.setAutoReconnect(true);
|
||||||
|
|
||||||
|
// Set static IP (prevents DHCP IP changes)
|
||||||
|
IPAddress staticIP(192, 168, 0, 181);
|
||||||
|
IPAddress gateway(192, 168, 0, 1);
|
||||||
|
IPAddress subnet(255, 255, 255, 0);
|
||||||
|
WiFi.config(staticIP, gateway, subnet);
|
||||||
|
|
||||||
|
// Connect to WiFi
|
||||||
|
Serial.print("Connecting to WiFi: ");
|
||||||
|
Serial.println(ssid);
|
||||||
|
WiFi.begin(ssid, password);
|
||||||
|
|
||||||
|
int attempts = 0;
|
||||||
|
while (WiFi.status() != WL_CONNECTED && attempts < 40) { // 40 * 500ms = 20 seconds
|
||||||
|
delay(500);
|
||||||
|
Serial.print(".");
|
||||||
|
attempts++;
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.println(""); // New line after dots
|
||||||
|
|
||||||
|
// Check WiFi status
|
||||||
|
int wifiStatus = WiFi.status();
|
||||||
|
if (wifiStatus == WL_CONNECTED) {
|
||||||
|
Serial.println("\n✓ WiFi connected!");
|
||||||
|
Serial.print("IP address: ");
|
||||||
|
Serial.println(WiFi.localIP());
|
||||||
|
Serial.print("RSSI: ");
|
||||||
|
Serial.print(WiFi.RSSI());
|
||||||
|
Serial.println(" dBm");
|
||||||
|
Serial.print("MAC: ");
|
||||||
|
Serial.println(WiFi.macAddress());
|
||||||
|
} else {
|
||||||
|
Serial.println("\n✗ WiFi connection failed!");
|
||||||
|
Serial.print("WiFi Status Code: ");
|
||||||
|
Serial.println(wifiStatus);
|
||||||
|
// Status codes: 0=IDLE, 1=NO_SSID, 2=SCAN_COMPLETE, 3=CONNECTED, 4=CONNECT_FAILED, 5=CONNECTION_LOST, 6=DISCONNECTED
|
||||||
|
switch(wifiStatus) {
|
||||||
|
case WL_NO_SSID_AVAIL:
|
||||||
|
Serial.println("ERROR: SSID not found! Check network name.");
|
||||||
|
break;
|
||||||
|
case WL_CONNECT_FAILED:
|
||||||
|
Serial.println("ERROR: Connection failed! Check password.");
|
||||||
|
break;
|
||||||
|
case WL_CONNECTION_LOST:
|
||||||
|
Serial.println("ERROR: Connection lost.");
|
||||||
|
break;
|
||||||
|
case WL_DISCONNECTED:
|
||||||
|
Serial.println("ERROR: Disconnected from network.");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Serial.println("ERROR: Unknown WiFi error.");
|
||||||
|
}
|
||||||
|
Serial.println("Continuing anyway to allow API access...");
|
||||||
|
|
||||||
|
// Scan and show available networks
|
||||||
|
scanWiFiNetworks();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup API endpoints
|
||||||
|
server.on("/", handleRoot);
|
||||||
|
server.on("/api/status", HTTP_GET, handleStatus);
|
||||||
|
// Relay endpoints
|
||||||
|
server.on("/relay/on", HTTP_POST, handleRelayOn);
|
||||||
|
server.on("/relay/off", HTTP_POST, handleRelayOff);
|
||||||
|
server.on("/relay/status", HTTP_GET, handleRelayStatus);
|
||||||
|
// Input endpoints
|
||||||
|
server.on("/input/status", HTTP_GET, handleInputStatus);
|
||||||
|
// Home Assistant webhook registration
|
||||||
|
server.on("/register", HTTP_POST, handleRegister);
|
||||||
|
// LED endpoints
|
||||||
|
server.on("/led/on", HTTP_POST, handleLEDOn);
|
||||||
|
server.on("/led/off", HTTP_POST, handleLEDOff);
|
||||||
|
server.onNotFound(handleNotFound);
|
||||||
|
|
||||||
|
// Start server
|
||||||
|
server.begin();
|
||||||
|
Serial.println("\n✓ HTTP server started on port 80");
|
||||||
|
Serial.println("\n=================================");
|
||||||
|
Serial.println("Ready! Try these endpoints:");
|
||||||
|
Serial.print(" http://");
|
||||||
|
Serial.print(WiFi.localIP());
|
||||||
|
Serial.println("/api/status");
|
||||||
|
Serial.println("=================================\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
server.handleClient();
|
||||||
|
|
||||||
|
// Simulate temperature reading
|
||||||
|
temperature = 25.0 + (random(-20, 20) / 10.0);
|
||||||
|
|
||||||
|
// Check for input state changes every 50ms
|
||||||
|
if (millis() - last_input_check > 50) {
|
||||||
|
last_input_check = millis();
|
||||||
|
checkInputChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
delay(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// WiFi Debug Function
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
void scanWiFiNetworks() {
|
||||||
|
Serial.println("\n--- Available WiFi Networks ---");
|
||||||
|
int n = WiFi.scanNetworks();
|
||||||
|
if (n == 0) {
|
||||||
|
Serial.println("No networks found!");
|
||||||
|
} else {
|
||||||
|
Serial.print("Found ");
|
||||||
|
Serial.print(n);
|
||||||
|
Serial.println(" networks:");
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
Serial.print(i + 1);
|
||||||
|
Serial.print(". ");
|
||||||
|
Serial.print(WiFi.SSID(i));
|
||||||
|
Serial.print(" (");
|
||||||
|
Serial.print(WiFi.RSSI(i));
|
||||||
|
Serial.print(" dBm) ");
|
||||||
|
Serial.println(WiFi.encryptionType(i) == WIFI_AUTH_OPEN ? "Open" : "Secured");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Serial.println("-------------------------------\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// API Handlers
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
void handleRoot() {
|
||||||
|
// Read all inputs first to get current state
|
||||||
|
input1_state = digitalRead(DIN1_PIN);
|
||||||
|
input2_state = digitalRead(DIN2_PIN);
|
||||||
|
input3_state = digitalRead(DIN3_PIN);
|
||||||
|
input4_state = digitalRead(DIN4_PIN);
|
||||||
|
|
||||||
|
String html = "<html><head><title>ESP32-C6 Device</title>";
|
||||||
|
html += "<meta name='viewport' content='width=device-width, initial-scale=1'>";
|
||||||
|
html += "<style>";
|
||||||
|
html += "body{font-family:Arial;margin:20px;background:#f0f0f0}";
|
||||||
|
html += ".card{background:white;padding:20px;margin:10px 0;border-radius:8px;box-shadow:0 2px 4px rgba(0,0,0,0.1)}";
|
||||||
|
html += "h1{color:#333}h2{color:#555;font-size:17px;margin-top:0;margin-bottom:12px}";
|
||||||
|
html += ".grid2{display:grid;grid-template-columns:1fr 1fr;gap:10px}";
|
||||||
|
// Input item with round LED indicator
|
||||||
|
html += ".inp-item{display:flex;align-items:center;gap:12px;padding:10px 14px;background:#f7f7f7;border-radius:8px;border:1px solid #e0e0e0}";
|
||||||
|
html += ".led{width:22px;height:22px;border-radius:50%;flex-shrink:0;transition:background 0.3s,box-shadow 0.3s}";
|
||||||
|
html += ".led-on{background:#4CAF50;box-shadow:0 0 8px #4CAF5099}";
|
||||||
|
html += ".led-off{background:#f44336;box-shadow:0 0 8px #f4433666}";
|
||||||
|
html += ".inp-name{font-weight:bold;font-size:14px;color:#333}";
|
||||||
|
html += ".inp-state{font-size:11px;color:#888;margin-top:2px}";
|
||||||
|
// Relay toggle buttons
|
||||||
|
html += ".relay-btn{width:100%;padding:14px 8px;border:none;border-radius:8px;cursor:pointer;font-size:14px;font-weight:bold;transition:background 0.2s,transform 0.1s}";
|
||||||
|
html += ".relay-btn:active{transform:scale(0.97)}";
|
||||||
|
html += ".relay-on{background:#4CAF50;color:white}";
|
||||||
|
html += ".relay-off{background:#9e9e9e;color:white}";
|
||||||
|
html += ".relay-on:hover{background:#43A047}.relay-off:hover{background:#757575}";
|
||||||
|
// LED control & webhook
|
||||||
|
html += ".card-row{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin:10px 0}"; // two big columns
|
||||||
|
html += ".card-row .card{margin:0}"; // reset per-card margin inside row
|
||||||
|
html += ".btn{padding:8px 16px;margin:4px;border:none;border-radius:4px;cursor:pointer;font-size:14px}";
|
||||||
|
html += ".btn-on{background:#4CAF50;color:white}.btn-off{background:#f44336;color:white}";
|
||||||
|
html += ".wh-ok{background:#c8e6c9;color:#1b5e20;padding:10px;border-radius:4px}";
|
||||||
|
html += ".wh-err{background:#ffcdd2;color:#b71c1c;padding:10px;border-radius:4px}";
|
||||||
|
html += "</style>";
|
||||||
|
html += "<script>";
|
||||||
|
// Relay toggle: read current state from data-state attribute, POST, update DOM
|
||||||
|
html += "function toggleRelay(n){";
|
||||||
|
html += "var b=document.getElementById('r'+n);";
|
||||||
|
html += "var on=b.dataset.state==='1';";
|
||||||
|
html += "fetch((on?'/relay/off':'/relay/on')+'?relay='+n,{method:'POST'})";
|
||||||
|
html += ".then(function(){var ns=!on;b.dataset.state=ns?'1':'0';";
|
||||||
|
html += "b.className='relay-btn '+(ns?'relay-on':'relay-off');";
|
||||||
|
html += "b.textContent='Relay '+n+': '+(ns?'ON':'OFF');})";
|
||||||
|
html += ".catch(function(e){console.error(e);});";
|
||||||
|
html += "}";
|
||||||
|
// Polling: update LED indicators and relay button states
|
||||||
|
html += "function updateStatus(){fetch('/api/status').then(function(r){return r.json();}).then(function(d){";
|
||||||
|
html += "for(var i=1;i<=4;i++){";
|
||||||
|
// input: raw HIGH=not pressed, so pressed = !d['input'+i]
|
||||||
|
html += "var p=!d['input'+i];";
|
||||||
|
html += "document.getElementById('led'+i).className='led '+(p?'led-on':'led-off');";
|
||||||
|
html += "document.getElementById('is'+i).textContent=p?'PRESSED':'NOT PRESSED';";
|
||||||
|
// relay: update toggle button
|
||||||
|
html += "var b=document.getElementById('r'+i);var on=d['relay'+i];";
|
||||||
|
html += "b.dataset.state=on?'1':'0';";
|
||||||
|
html += "b.className='relay-btn '+(on?'relay-on':'relay-off');";
|
||||||
|
html += "b.textContent='Relay '+i+': '+(on?'ON':'OFF');";
|
||||||
|
html += "}";
|
||||||
|
html += "}).catch(function(e){console.error(e);});}";
|
||||||
|
html += "window.addEventListener('load',function(){updateStatus();setInterval(updateStatus,2000);});";
|
||||||
|
html += "</script>";
|
||||||
|
html += "</head><body>";
|
||||||
|
html += "<h1>ESP32-C6 Control Panel</h1>";
|
||||||
|
|
||||||
|
// Device Info
|
||||||
|
html += "<div class='card'><h2>Device Info</h2>";
|
||||||
|
html += "IP: " + WiFi.localIP().toString() + " ";
|
||||||
|
html += "RSSI: " + String(WiFi.RSSI()) + " dBm ";
|
||||||
|
html += "Temp: " + String(temperature, 1) + "°C ";
|
||||||
|
html += "Uptime: " + String(millis() / 1000) + "s</div>";
|
||||||
|
|
||||||
|
// Inputs + Relays side by side in a 2-column row
|
||||||
|
html += "<div class='card-row'>";
|
||||||
|
|
||||||
|
// Inputs — 2-column inner grid, round LED indicator
|
||||||
|
html += "<div class='card'><h2>Inputs</h2><div class='grid2'>";
|
||||||
|
bool inputStates[5] = {false, input1_state, input2_state, input3_state, input4_state};
|
||||||
|
for (int i = 1; i <= 4; i++) {
|
||||||
|
bool pressed = !inputStates[i]; // pull-up: LOW=pressed
|
||||||
|
html += "<div class='inp-item'>";
|
||||||
|
html += "<div class='led " + String(pressed ? "led-on" : "led-off") + "' id='led" + String(i) + "'></div>";
|
||||||
|
html += "<div><div class='inp-name'>Input " + String(i) + "</div>";
|
||||||
|
html += "<div class='inp-state' id='is" + String(i) + "'>" + String(pressed ? "PRESSED" : "NOT PRESSED") + "</div></div>";
|
||||||
|
html += "</div>";
|
||||||
|
}
|
||||||
|
html += "</div></div>"; // close Inputs card
|
||||||
|
|
||||||
|
// Relays — 2-column inner grid, toggle buttons
|
||||||
|
html += "<div class='card'><h2>Relays</h2><div class='grid2'>";
|
||||||
|
bool relayStates[5] = {false, relay1_state, relay2_state, relay3_state, relay4_state};
|
||||||
|
for (int i = 1; i <= 4; i++) {
|
||||||
|
bool on = relayStates[i];
|
||||||
|
html += "<button class='relay-btn " + String(on ? "relay-on" : "relay-off") + "' ";
|
||||||
|
html += "id='r" + String(i) + "' data-state='" + String(on ? "1" : "0") + "' ";
|
||||||
|
html += "onclick='toggleRelay(" + String(i) + ")'>";
|
||||||
|
html += "Relay " + String(i) + ": " + String(on ? "ON" : "OFF");
|
||||||
|
html += "</button>";
|
||||||
|
}
|
||||||
|
html += "</div></div>"; // close Relays card
|
||||||
|
html += "</div>"; // close .card-row
|
||||||
|
|
||||||
|
// LED Control
|
||||||
|
html += "<div class='card'><h2>LED Control</h2>";
|
||||||
|
html += "<button class='btn btn-on' onclick='fetch(\"/led/on\",{method:\"POST\"}).then(()=>location.reload())'>LED ON</button>";
|
||||||
|
html += "<button class='btn btn-off' onclick='fetch(\"/led/off\",{method:\"POST\"}).then(()=>location.reload())'>LED OFF</button>";
|
||||||
|
html += " Status: <strong>" + String(led_state ? "ON" : "OFF") + "</strong></div>";
|
||||||
|
|
||||||
|
// Home Assistant Webhook Status
|
||||||
|
html += "<div class='card'><h2>Home Assistant Webhook</h2>";
|
||||||
|
if (ha_registered && strlen(ha_callback_url) > 0) {
|
||||||
|
html += "<div class='wh-ok'>✓ Connected — " + String(ha_callback_url) + "</div>";
|
||||||
|
} else {
|
||||||
|
html += "<div class='wh-err'>✗ Not registered — waiting for Home Assistant...</div>";
|
||||||
|
}
|
||||||
|
html += "</div>";
|
||||||
|
|
||||||
|
html += "<div class='card'><h2>API Endpoints</h2>";
|
||||||
|
html += "GET /api/status POST /relay/on?relay=1-4 POST /relay/off?relay=1-4<br>";
|
||||||
|
html += "GET /input/status?input=1-4 POST /led/on POST /led/off<br>";
|
||||||
|
html += "POST /register?callback_url=...</div>";
|
||||||
|
|
||||||
|
html += "</body></html>";
|
||||||
|
|
||||||
|
server.send(200, "text/html", html);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status request monitoring
|
||||||
|
static unsigned long last_status_log = 0;
|
||||||
|
static int status_request_count = 0;
|
||||||
|
|
||||||
|
void handleStatus() {
|
||||||
|
status_request_count++;
|
||||||
|
|
||||||
|
// Read fresh input states
|
||||||
|
input1_state = digitalRead(DIN1_PIN);
|
||||||
|
input2_state = digitalRead(DIN2_PIN);
|
||||||
|
input3_state = digitalRead(DIN3_PIN);
|
||||||
|
input4_state = digitalRead(DIN4_PIN);
|
||||||
|
|
||||||
|
String json = "{";
|
||||||
|
json += "\"input1\":" + String(input1_state ? "true" : "false") + ",";
|
||||||
|
json += "\"input2\":" + String(input2_state ? "true" : "false") + ",";
|
||||||
|
json += "\"input3\":" + String(input3_state ? "true" : "false") + ",";
|
||||||
|
json += "\"input4\":" + String(input4_state ? "true" : "false") + ",";
|
||||||
|
json += "\"relay1\":" + String(relay1_state ? "true" : "false") + ",";
|
||||||
|
json += "\"relay2\":" + String(relay2_state ? "true" : "false") + ",";
|
||||||
|
json += "\"relay3\":" + String(relay3_state ? "true" : "false") + ",";
|
||||||
|
json += "\"relay4\":" + String(relay4_state ? "true" : "false") + ",";
|
||||||
|
json += "\"led\":" + String(led_state ? "true" : "false");
|
||||||
|
json += "}";
|
||||||
|
|
||||||
|
server.send(200, "application/json", json);
|
||||||
|
|
||||||
|
// Log status every 10 seconds
|
||||||
|
if (millis() - last_status_log > 10000) {
|
||||||
|
last_status_log = millis();
|
||||||
|
Serial.printf("API: %d requests/10sec, Free heap: %d bytes, Uptime: %lus\n", status_request_count, ESP.getFreeHeap(), millis() / 1000);
|
||||||
|
status_request_count = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleRelayOn() {
|
||||||
|
if (!server.hasArg("relay")) {
|
||||||
|
server.send(400, "application/json", "{\"error\":\"Missing relay parameter\"}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int relay_num = server.arg("relay").toInt();
|
||||||
|
int pin = -1;
|
||||||
|
bool *state_ptr = nullptr;
|
||||||
|
|
||||||
|
switch(relay_num) {
|
||||||
|
case 1: pin = RELAY_1_PIN; state_ptr = &relay1_state; break;
|
||||||
|
case 2: pin = RELAY_2_PIN; state_ptr = &relay2_state; break;
|
||||||
|
case 3: pin = RELAY_3_PIN; state_ptr = &relay3_state; break;
|
||||||
|
case 4: pin = RELAY_4_PIN; state_ptr = &relay4_state; break;
|
||||||
|
default:
|
||||||
|
server.send(400, "application/json", "{\"error\":\"Invalid relay number\"}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
digitalWrite(pin, HIGH);
|
||||||
|
*state_ptr = true;
|
||||||
|
server.send(200, "application/json", "{\"status\":\"ok\",\"state\":true}");
|
||||||
|
Serial.printf("Relay %d ON\n", relay_num);
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleRelayOff() {
|
||||||
|
if (!server.hasArg("relay")) {
|
||||||
|
server.send(400, "application/json", "{\"error\":\"Missing relay parameter\"}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int relay_num = server.arg("relay").toInt();
|
||||||
|
int pin = -1;
|
||||||
|
bool *state_ptr = nullptr;
|
||||||
|
|
||||||
|
switch(relay_num) {
|
||||||
|
case 1: pin = RELAY_1_PIN; state_ptr = &relay1_state; break;
|
||||||
|
case 2: pin = RELAY_2_PIN; state_ptr = &relay2_state; break;
|
||||||
|
case 3: pin = RELAY_3_PIN; state_ptr = &relay3_state; break;
|
||||||
|
case 4: pin = RELAY_4_PIN; state_ptr = &relay4_state; break;
|
||||||
|
default:
|
||||||
|
server.send(400, "application/json", "{\"error\":\"Invalid relay number\"}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
digitalWrite(pin, LOW);
|
||||||
|
*state_ptr = false;
|
||||||
|
server.send(200, "application/json", "{\"status\":\"ok\",\"state\":false}");
|
||||||
|
Serial.printf("Relay %d OFF\n", relay_num);
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleRelayStatus() {
|
||||||
|
if (!server.hasArg("relay")) {
|
||||||
|
server.send(400, "application/json", "{\"error\":\"Missing relay parameter\"}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int relay_num = server.arg("relay").toInt();
|
||||||
|
bool state = false;
|
||||||
|
|
||||||
|
switch(relay_num) {
|
||||||
|
case 1: state = relay1_state; break;
|
||||||
|
case 2: state = relay2_state; break;
|
||||||
|
case 3: state = relay3_state; break;
|
||||||
|
case 4: state = relay4_state; break;
|
||||||
|
default:
|
||||||
|
server.send(400, "application/json", "{\"error\":\"Invalid relay number\"}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String json = "{\"state\":" + String(state ? "true" : "false") + "}";
|
||||||
|
server.send(200, "application/json", json);
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleInputStatus() {
|
||||||
|
if (!server.hasArg("input")) {
|
||||||
|
server.send(400, "application/json", "{\"error\":\"Missing input parameter\"}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int input_num = server.arg("input").toInt();
|
||||||
|
int pin = -1;
|
||||||
|
|
||||||
|
switch(input_num) {
|
||||||
|
case 1: pin = DIN1_PIN; break;
|
||||||
|
case 2: pin = DIN2_PIN; break;
|
||||||
|
case 3: pin = DIN3_PIN; break;
|
||||||
|
case 4: pin = DIN4_PIN; break;
|
||||||
|
default:
|
||||||
|
server.send(400, "application/json", "{\"error\":\"Invalid input number\"}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int level = digitalRead(pin);
|
||||||
|
String json = "{\"state\":" + String(level ? "true" : "false") + "}";
|
||||||
|
server.send(200, "application/json", json);
|
||||||
|
Serial.printf("Input %d status: %d\n", input_num, level);
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleLEDOn() {
|
||||||
|
digitalWrite(LED_PIN, LOW); // LED is active-low
|
||||||
|
led_state = true;
|
||||||
|
server.send(200, "application/json", "{\"status\":\"ok\"}");
|
||||||
|
Serial.println("LED ON");
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleLEDOff() {
|
||||||
|
digitalWrite(LED_PIN, HIGH); // LED is active-low
|
||||||
|
led_state = false;
|
||||||
|
server.send(200, "application/json", "{\"status\":\"ok\"}");
|
||||||
|
Serial.println("LED OFF");
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleNotFound() {
|
||||||
|
String message = "404: Not Found\n\n";
|
||||||
|
message += "URI: " + server.uri() + "\n";
|
||||||
|
message += "Method: " + String((server.method() == HTTP_GET) ? "GET" : "POST") + "\n";
|
||||||
|
server.send(404, "text/plain", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Home Assistant Integration
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
void handleRegister() {
|
||||||
|
if (!server.hasArg("callback_url")) {
|
||||||
|
server.send(400, "application/json", "{\"error\":\"Missing callback_url parameter\"}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String url = server.arg("callback_url");
|
||||||
|
if (url.length() > 255) {
|
||||||
|
server.send(400, "application/json", "{\"error\":\"URL too long (max 255 chars)\"}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
url.toCharArray(ha_callback_url, 256);
|
||||||
|
ha_registered = true;
|
||||||
|
|
||||||
|
Serial.printf("Home Assistant webhook registered: %s\n", ha_callback_url);
|
||||||
|
server.send(200, "application/json", "{\"status\":\"ok\",\"message\":\"Webhook registered\"}");
|
||||||
|
}
|
||||||
|
|
||||||
|
void checkInputChanges() {
|
||||||
|
if (!ha_registered) return; // Only check if HA is registered
|
||||||
|
|
||||||
|
// Read all input states
|
||||||
|
bool curr_input1 = digitalRead(DIN1_PIN);
|
||||||
|
bool curr_input2 = digitalRead(DIN2_PIN);
|
||||||
|
bool curr_input3 = digitalRead(DIN3_PIN);
|
||||||
|
bool curr_input4 = digitalRead(DIN4_PIN);
|
||||||
|
|
||||||
|
// Check for changes and POST events
|
||||||
|
if (curr_input1 != last_input1_state) {
|
||||||
|
last_input1_state = curr_input1;
|
||||||
|
postInputEvent(1, curr_input1);
|
||||||
|
}
|
||||||
|
if (curr_input2 != last_input2_state) {
|
||||||
|
last_input2_state = curr_input2;
|
||||||
|
postInputEvent(2, curr_input2);
|
||||||
|
}
|
||||||
|
if (curr_input3 != last_input3_state) {
|
||||||
|
last_input3_state = curr_input3;
|
||||||
|
postInputEvent(3, curr_input3);
|
||||||
|
}
|
||||||
|
if (curr_input4 != last_input4_state) {
|
||||||
|
last_input4_state = curr_input4;
|
||||||
|
postInputEvent(4, curr_input4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void postInputEvent(int input_num, bool state) {
|
||||||
|
if (!ha_registered || strlen(ha_callback_url) == 0) {
|
||||||
|
return; // Not registered, skip
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invert state because inputs use pull-up (HIGH=not pressed, LOW=pressed)
|
||||||
|
bool pressed = !state;
|
||||||
|
String event_type = pressed ? "input_on" : "input_off";
|
||||||
|
|
||||||
|
Serial.printf("Input %d event: %s (raw_state=%d) - POSTing to HA\n", input_num, event_type.c_str(), state);
|
||||||
|
|
||||||
|
// Parse callback URL (format: http://host:port/path)
|
||||||
|
String url_str = String(ha_callback_url);
|
||||||
|
|
||||||
|
// Extract host and path
|
||||||
|
int protocol_end = url_str.indexOf("://");
|
||||||
|
if (protocol_end < 0) return;
|
||||||
|
|
||||||
|
int host_start = protocol_end + 3;
|
||||||
|
int port_separator = url_str.indexOf(":", host_start);
|
||||||
|
int path_start = url_str.indexOf("/", host_start);
|
||||||
|
|
||||||
|
if (path_start < 0) path_start = url_str.length();
|
||||||
|
if (port_separator < 0 || port_separator > path_start) port_separator = -1;
|
||||||
|
|
||||||
|
String host = url_str.substring(host_start, (port_separator >= 0) ? port_separator : path_start);
|
||||||
|
int port = 80; // Default HTTP port
|
||||||
|
if (port_separator >= 0) {
|
||||||
|
int colon_port_end = url_str.indexOf("/", port_separator);
|
||||||
|
if (colon_port_end < 0) colon_port_end = url_str.length();
|
||||||
|
String port_str = url_str.substring(port_separator + 1, colon_port_end);
|
||||||
|
port = port_str.toInt();
|
||||||
|
}
|
||||||
|
String path = url_str.substring(path_start);
|
||||||
|
|
||||||
|
// Create JSON payload
|
||||||
|
String json = "{\"input\":" + String(input_num) + ",\"state\":" + (pressed ? "true" : "false") + "}";
|
||||||
|
|
||||||
|
// Connect to Home Assistant and POST
|
||||||
|
WiFiClient client;
|
||||||
|
if (!client.connect(host.c_str(), port)) {
|
||||||
|
Serial.printf("Failed to connect to %s:%d\n", host.c_str(), port);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send HTTP POST request
|
||||||
|
client.println("POST " + path + " HTTP/1.1");
|
||||||
|
client.println("Host: " + host);
|
||||||
|
client.println("Content-Type: application/json");
|
||||||
|
client.println("Content-Length: " + String(json.length()));
|
||||||
|
client.println("Connection: close");
|
||||||
|
client.println();
|
||||||
|
client.print(json);
|
||||||
|
|
||||||
|
// Wait for response
|
||||||
|
delay(100);
|
||||||
|
|
||||||
|
// Read response
|
||||||
|
while (client.available()) {
|
||||||
|
char c = client.read();
|
||||||
|
// Just discard the response for now
|
||||||
|
}
|
||||||
|
|
||||||
|
client.stop();
|
||||||
|
Serial.printf("Input %d event posted successfully\n", input_num);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user