commit b1ee2610ca592e5e41f1f0ad573302f7bcd8c55f Author: ske087 Date: Mon Feb 23 21:10:38 2026 +0200 Initial commit: Olimex ESP32-C6-EVB HA integration + Arduino sketch diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fc51a07 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +# Python +__pycache__/ +*.pyc +*.pyo +*.pyd +.Python + +# Home Assistant generated +icon.png + +# OS +.DS_Store +Thumbs.db diff --git a/README.md b/README.md new file mode 100644 index 0000000..ad0569d --- /dev/null +++ b/README.md @@ -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. diff --git a/custom_components/olimex_esp32_c6/README.md b/custom_components/olimex_esp32_c6/README.md new file mode 100644 index 0000000..76f7579 --- /dev/null +++ b/custom_components/olimex_esp32_c6/README.md @@ -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://:/api/status +Response: { + "temperature": 25.5, + "wifi_rssi": -45 +} +``` + +#### Relay Control +``` +POST http://:/api/relay//on +POST http://:/api/relay//off +GET http://:/api/relay//status +Response: {"state": true} +``` + +#### LED Control +``` +POST http://:/api/led//on +POST http://:/api/led//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 +#include + +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/) diff --git a/custom_components/olimex_esp32_c6/SETUP.md b/custom_components/olimex_esp32_c6/SETUP.md new file mode 100644 index 0000000..77d4a45 --- /dev/null +++ b/custom_components/olimex_esp32_c6/SETUP.md @@ -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 diff --git a/custom_components/olimex_esp32_c6/__init__.py b/custom_components/olimex_esp32_c6/__init__.py new file mode 100644 index 0000000..cebb62d --- /dev/null +++ b/custom_components/olimex_esp32_c6/__init__.py @@ -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 diff --git a/custom_components/olimex_esp32_c6/binary_sensor.py b/custom_components/olimex_esp32_c6/binary_sensor.py new file mode 100644 index 0000000..7f6e077 --- /dev/null +++ b/custom_components/olimex_esp32_c6/binary_sensor.py @@ -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", + } diff --git a/custom_components/olimex_esp32_c6/config_flow.py b/custom_components/olimex_esp32_c6/config_flow.py new file mode 100644 index 0000000..dc0eafb --- /dev/null +++ b/custom_components/olimex_esp32_c6/config_flow.py @@ -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), + }, + ) diff --git a/custom_components/olimex_esp32_c6/const.py b/custom_components/olimex_esp32_c6/const.py new file mode 100644 index 0000000..84066fc --- /dev/null +++ b/custom_components/olimex_esp32_c6/const.py @@ -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, +} diff --git a/custom_components/olimex_esp32_c6/manifest.json b/custom_components/olimex_esp32_c6/manifest.json new file mode 100644 index 0000000..8abc83f --- /dev/null +++ b/custom_components/olimex_esp32_c6/manifest.json @@ -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" +} diff --git a/custom_components/olimex_esp32_c6/olimex.png b/custom_components/olimex_esp32_c6/olimex.png new file mode 100644 index 0000000..5b4e72a Binary files /dev/null and b/custom_components/olimex_esp32_c6/olimex.png differ diff --git a/custom_components/olimex_esp32_c6/sensor.py b/custom_components/olimex_esp32_c6/sensor.py new file mode 100644 index 0000000..2da4062 --- /dev/null +++ b/custom_components/olimex_esp32_c6/sensor.py @@ -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 diff --git a/custom_components/olimex_esp32_c6/sensor_updater.py b/custom_components/olimex_esp32_c6/sensor_updater.py new file mode 100644 index 0000000..4dfb947 --- /dev/null +++ b/custom_components/olimex_esp32_c6/sensor_updater.py @@ -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", + } diff --git a/custom_components/olimex_esp32_c6/strings.json b/custom_components/olimex_esp32_c6/strings.json new file mode 100644 index 0000000..a1a79e1 --- /dev/null +++ b/custom_components/olimex_esp32_c6/strings.json @@ -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" + } + } + } +} diff --git a/custom_components/olimex_esp32_c6/switch.py b/custom_components/olimex_esp32_c6/switch.py new file mode 100644 index 0000000..2419d20 --- /dev/null +++ b/custom_components/olimex_esp32_c6/switch.py @@ -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", + } diff --git a/custom_components/olimex_esp32_c6/webhook.py b/custom_components/olimex_esp32_c6/webhook.py new file mode 100644 index 0000000..247b338 --- /dev/null +++ b/custom_components/olimex_esp32_c6/webhook.py @@ -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) diff --git a/esp32_arduino/DEPLOYMENT_GUIDE.md b/esp32_arduino/DEPLOYMENT_GUIDE.md new file mode 100644 index 0000000..41a8536 --- /dev/null +++ b/esp32_arduino/DEPLOYMENT_GUIDE.md @@ -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/) + diff --git a/esp32_arduino/README.md b/esp32_arduino/README.md new file mode 100644 index 0000000..f8f8bee --- /dev/null +++ b/esp32_arduino/README.md @@ -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:///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:///api/relay/relay_1/on + +# Turn OFF +curl -X POST http:///api/relay/relay_1/off + +# Get Status +curl http:///api/relay/relay_1/status +``` + +#### Control LED +```bash +# Turn ON +curl -X POST http:///api/led/led/on + +# Turn OFF +curl -X POST http:///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/` diff --git a/esp32_arduino/esp32_arduino.ino b/esp32_arduino/esp32_arduino.ino new file mode 100644 index 0000000..1b8aacf --- /dev/null +++ b/esp32_arduino/esp32_arduino.ino @@ -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 +#include +#include + +// 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 = "ESP32-C6 Device"; + html += ""; + html += ""; + html += ""; + html += ""; + html += "

ESP32-C6 Control Panel

"; + + // Device Info + html += "

Device Info

"; + html += "IP: " + WiFi.localIP().toString() + "   "; + html += "RSSI: " + String(WiFi.RSSI()) + " dBm   "; + html += "Temp: " + String(temperature, 1) + "°C   "; + html += "Uptime: " + String(millis() / 1000) + "s
"; + + // Inputs + Relays side by side in a 2-column row + html += "
"; + + // Inputs — 2-column inner grid, round LED indicator + html += "

Inputs

"; + 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 += "
"; + html += "
"; + html += "
Input " + String(i) + "
"; + html += "
" + String(pressed ? "PRESSED" : "NOT PRESSED") + "
"; + html += "
"; + } + html += "
"; // close Inputs card + + // Relays — 2-column inner grid, toggle buttons + html += "

Relays

"; + bool relayStates[5] = {false, relay1_state, relay2_state, relay3_state, relay4_state}; + for (int i = 1; i <= 4; i++) { + bool on = relayStates[i]; + html += ""; + } + html += "
"; // close Relays card + html += "
"; // close .card-row + + // LED Control + html += "

LED Control

"; + html += ""; + html += ""; + html += "   Status: " + String(led_state ? "ON" : "OFF") + "
"; + + // Home Assistant Webhook Status + html += "

Home Assistant Webhook

"; + if (ha_registered && strlen(ha_callback_url) > 0) { + html += "
✓ Connected — " + String(ha_callback_url) + "
"; + } else { + html += "
✗ Not registered — waiting for Home Assistant...
"; + } + html += "
"; + + html += "

API Endpoints

"; + html += "GET /api/status   POST /relay/on?relay=1-4   POST /relay/off?relay=1-4
"; + html += "GET /input/status?input=1-4   POST /led/on   POST /led/off
"; + html += "POST /register?callback_url=...
"; + + 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); +}