I finally mastered the one-wire setup Freddie showed at the start of this topic too (besides the 2 wire setup using optocouplers - see earlier post), albeit with a twist. I’ve spelled it out below, because the pictures of Freddie left a bit too much to the imagination if you are a complete idiot (like me) when it comes to volts and soldering and stuff. As a consequence it took me one and half year (with pauses) to complete this and not Freddie’s 1,5 hour. But I got there in the end and had some fun (and lots of frustration) along the way. Hope that someone feels inspired to get soldering and, foremost, to get yourself an ESP32 and start playing with them, they are awesome.
Here are the schematics, a final picture, the shopping list (with links to AliExpress) and the ESP-IDF code (not Arduino).
Shopping List:
- Seeed Studio XIAO ESP32C6 (because it is small and wifi6)
- 3.3-6V to 3V DC-DC Step-Down Power Supply Buck LDO Module Voltage regulator Board (because the Seeed board spits out 3.3V and the remote operates at 3.0V)
Schematics
So, the Seeed Xiao ESP32-C6 puts 3.3V as an output to the GPIO port(s) and of course to the 3.3v power output. The remote operates with 2 AAA batteries meaning 3.0V, hence the buck converters.
The first (lowest) converter provides the remote with uninterrupted 3v power. The second from the bottom feeds a continuous 3v to the + of the OPEN button and is interrupted when the button is pressed in Home Assistant. The 3rd operates the STOP button and the 4th operates the CLOSE button in a similar way. The wires are connected to the + legs (use multimeter) of each button. Middle whole of each converter and the lowest connection on the ESP32 (closest to USB-C port) are all negatives (black wires) and are all coming/soldered together to the minus of the remote. The physical buttons of the remote still work once you’ve soldered the stuff together.
This does not look so nice as in the schematics, but with the knowledge I have now, I would first glue the buck converters to some carton or plastic, nice and tidy together, then solder the wires (for which I used wires from a CAT6 POE cable) to the converters, glue them as well, so they can’t move and then fit the whole inside the remote opening where the batteries were. Lastly then to solder the wires to the ESP32 and the buttons on the board and also place the esp32 inside the remote housing. It should nicely fit, there is enough space.
main.c code
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/timers.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "nvs_flash.h"
#include "esp_log.h"
#include "esp_system.h"
#include "esp_err.h"
#include "secret.h"
#include "mqtt_client.h"
#include "driver/gpio.h"
// Function prototypes
void simulate_button_press(int gpio_pin);
#define TAG "awning_example"
#define GPIO_OPEN 19 // GPIO pin for OPEN button
#define GPIO_STOP 17 // GPIO pin for STOP button
#define GPIO_CLOSE 20 // GPIO pin for CLOSE button
#define GPIO_LED 15 // Onboard LED
// Adjustable parameters
#define FULL_MOVEMENT_TIME 30000 // Time in MILLIseconds for full open/close
#define POSITION_STEP 2 // Percentage step for slider movement
// Global variables
static esp_mqtt_client_handle_t client;
static int current_position = 0; // Current awning position (0-100)
static int target_position = 0; // Target position (0-100)
static TimerHandle_t movement_timer; // Timer for controlling movement
// Timer callback function
void movement_timer_callback(TimerHandle_t xTimer) {
if (current_position == target_position) {
xTimerStop(movement_timer, 0); // Stop the timer if target is reached
ESP_LOGI(TAG, "Reached target position: %d%%", current_position);
// **Only send STOP when moving to a partial position (1-99%)**
if (target_position > 0 && target_position < 100) {
simulate_button_press(GPIO_STOP);
// Send STOP to MQTT (so Home Assistant knows movement ended)
esp_mqtt_client_publish(client, "awning_1/state", "STOP", 0, 1, 1);
}
// Send correct MQTT state based on position instead of STOP
if (current_position == 100) {
esp_mqtt_client_publish(client, "awning_1/state", "OPEN", 0, 1, 1);
} else if (current_position == 0) {
esp_mqtt_client_publish(client, "awning_1/state", "CLOSED", 0, 1, 1);
} else {
esp_mqtt_client_publish(client, "awning_1/state", "PARTIAL", 0, 1, 1);
}
return;
}
int direction = (target_position > current_position) ? 1 : -1;
current_position += direction * POSITION_STEP;
if ((direction == 1 && current_position > target_position) ||
(direction == -1 && current_position < target_position)) {
current_position = target_position;
}
ESP_LOGI(TAG, "Updated position: %d%%", current_position);
char position_str[4];
sprintf(position_str, "%d", current_position);
esp_mqtt_client_publish(client, "awning_1/position", position_str, 0, 1, 1);
}
// Start movement to target position
void move_to_position(int position) {
target_position = position;
// Determine direction of movement
int direction = (target_position > current_position) ? GPIO_OPEN : GPIO_CLOSE;
// Simulate the initial button press to start movement
simulate_button_press(direction);
int timer_period_ms = (FULL_MOVEMENT_TIME / 100) * POSITION_STEP;
ESP_LOGI(TAG, "Starting movement to %d%% with step size: %d%%", target_position, POSITION_STEP);
if (xTimerIsTimerActive(movement_timer) == pdTRUE) {
xTimerStop(movement_timer, 0);
}
xTimerChangePeriod(movement_timer, pdMS_TO_TICKS(timer_period_ms), 0);
xTimerStart(movement_timer, 0);
}
// Initialize movement timer
void init_movement_timer() {
movement_timer = xTimerCreate("MovementTimer", pdMS_TO_TICKS(1000), pdTRUE, NULL, movement_timer_callback);
if (movement_timer == NULL) {
ESP_LOGE(TAG, "Failed to create movement timer");
} else {
ESP_LOGI(TAG, "Movement timer initialized");
}
}
// Simulate button press
void simulate_button_press(int gpio_pin) {
gpio_set_level(gpio_pin, 0);
vTaskDelay(2000 / portTICK_PERIOD_MS);
gpio_set_level(gpio_pin, 1);
}
// Dynamic Blink LED Function
void blink_led(int repetitions, int duration_ms) {
for (int i = 0; i < repetitions; i++) {
gpio_set_level(GPIO_LED, 0); // Turn LED on
vTaskDelay(duration_ms / portTICK_PERIOD_MS);
gpio_set_level(GPIO_LED, 1); // Turn LED off
vTaskDelay(duration_ms / portTICK_PERIOD_MS);
}
}
// Event handler for catching system events
static void event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) {
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
esp_wifi_connect();
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
esp_wifi_connect();
ESP_LOGI(TAG, "Retry to connect to the AP");
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data;
ESP_LOGI(TAG, "got ip:" IPSTR, IP2STR(&event->ip_info.ip));
}
}
// MQTT event handler
static void mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) {
esp_mqtt_event_handle_t event = event_data;
client = event->client;
switch (event_id) {
case MQTT_EVENT_CONNECTED:
ESP_LOGI(TAG, "MQTT_EVENT_CONNECTED");
esp_mqtt_client_subscribe(client, "awning_1/command", 0);
break;
case MQTT_EVENT_DATA:
ESP_LOGI(TAG, "MQTT_EVENT_DATA received");
ESP_LOGI(TAG, "TOPIC=%.*s", event->topic_len, event->topic);
ESP_LOGI(TAG, "DATA=%.*s", event->data_len, event->data);
if (strncmp(event->topic, "awning_1/command", event->topic_len) == 0) {
ESP_LOGI(TAG, "Correct topic: %s", "awning_1/command");
if (strncmp(event->data, "OPEN", event->data_len) == 0) {
ESP_LOGI(TAG, "Command: OPEN awning");
simulate_button_press(GPIO_OPEN);
move_to_position(100); // Incrementally move to 100%
esp_mqtt_client_publish(client, "awning_1/state", "open", 0, 1, 1);
blink_led(3, 150); // Blink x times, y ms each for OPEN command
} else if (strncmp(event->data, "CLOSE", event->data_len) == 0) {
ESP_LOGI(TAG, "Command: CLOSE awning");
simulate_button_press(GPIO_CLOSE);
move_to_position(0); // Incrementally move to 0%
//esp_mqtt_client_publish(client, "awning_1/state", "closed", 0, 1, 1);
blink_led(1, 1000); // Blink x times, Y ms each for CLOSE command
} else if (strncmp(event->data, "STOP", event->data_len) == 0) {
ESP_LOGI(TAG, "Command: STOP awning");
simulate_button_press(GPIO_STOP);
esp_mqtt_client_publish(client, "awning_1/state", "stop", 0, 1, 1);
// Stop the movement timer ONLY when STOP is manually requested
if (xTimerIsTimerActive(movement_timer) == pdTRUE) {
xTimerStop(movement_timer, 0);
ESP_LOGI(TAG, "Movement timer stopped");
}
blink_led(3, 500); // Blink x times, y ms duration for STOP command
} else {
int position = atoi(event->data);
if (position >= 0 && position <= 100) {
ESP_LOGI(TAG, "Command: Set awning position to %d", position);
move_to_position(position); // Incrementally move to specified position
esp_mqtt_client_publish(client, "awning_1/state", "PARTIAL", 0, 1, 1);
} else {
ESP_LOGW(TAG, "Unknown command received: %.*s", event->data_len, event->data);
}
}
} else {
ESP_LOGW(TAG, "Received data on an unexpected topic: %.*s", event->topic_len, event->topic);
}
break;
case MQTT_EVENT_PUBLISHED:
ESP_LOGD(TAG, "MQTT_EVENT_PUBLISHED"); // Reduce verbosity
break;
default:
ESP_LOGI(TAG, "Other event id: %ld", (long)event_id);
break;
}
}
// WiFi initialization
void wifi_init_sta(void) {
esp_netif_init();
esp_event_loop_create_default();
esp_netif_create_default_wifi_sta();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
esp_wifi_init(&cfg);
esp_event_handler_instance_t instance_any_id;
esp_event_handler_instance_t instance_got_ip;
esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL, &instance_any_id);
esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL, &instance_got_ip);
wifi_config_t wifi_config = {
.sta = {
.ssid = WIFI_SSID,
.password = WIFI_PASSWORD,
.threshold.authmode = WIFI_AUTH_WPA2_PSK,
},
};
esp_wifi_set_mode(WIFI_MODE_STA);
esp_wifi_set_config(ESP_IF_WIFI_STA, &wifi_config);
esp_wifi_start();
ESP_LOGI(TAG, "wifi_init_sta finished.");
}
// GPIO initialization
void init_gpio() {
gpio_reset_pin(GPIO_OPEN);
gpio_set_direction(GPIO_OPEN, GPIO_MODE_OUTPUT);
gpio_set_level(GPIO_OPEN, 1);
gpio_reset_pin(GPIO_STOP);
gpio_set_direction(GPIO_STOP, GPIO_MODE_OUTPUT);
gpio_set_level(GPIO_STOP, 1);
gpio_reset_pin(GPIO_CLOSE);
gpio_set_direction(GPIO_CLOSE, GPIO_MODE_OUTPUT);
gpio_set_level(GPIO_CLOSE, 1);
gpio_reset_pin(GPIO_LED);
gpio_set_direction(GPIO_LED, GPIO_MODE_OUTPUT);
gpio_set_level(GPIO_LED, 1);
}
// MQTT app start
void mqtt_app_start(void) {
esp_mqtt_client_config_t mqtt_cfg = {
.broker = {
.address.uri = MQTT_BROKER_URI,
},
.credentials = {
.username = MQTT_USER,
.authentication = {
.password = MQTT_PASSWORD,
}
}
};
client = esp_mqtt_client_init(&mqtt_cfg);
esp_mqtt_client_register_event(client, ESP_EVENT_ANY_ID, mqtt_event_handler, client);
esp_mqtt_client_start(client);
}
// Main function
void app_main(void) {
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
wifi_init_sta();
init_gpio();
init_movement_timer(); // Initialize timer for awning movement
mqtt_app_start();
}
secret.h
#define WIFI_SSID "YOUR_SSID_HERE"
#define WIFI_PASSWORD "YOUR_WIFI_PASSWORD_HERE"
#define MQTT_BROKER_URI "mqtt://YOUR_IP_ADDRESS_HERE:1883"
#define MQTT_USER "YOUR_MQTT_USER_ID_HERE"
#define MQTT_PASSWORD "YOUR_MQTT_PW_HERE"
Configuration.yaml
mqtt:
cover:
- name: "Zonnescherm"
unique_id: awning01
#state_topic: "awning_1/state"
position_topic: "awning_1/position"
set_position_topic: "awning_1/command"
command_topic: "awning_1/command"
payload_open: "OPEN"
payload_close: "CLOSE"
payload_stop: "STOP"
qos: 0
retain: true
optimistic: false
device_class: "awning"
device:
identifiers: "awning_1"
name: "Zonnescherm"
model: "ESP32-C6 RC Manager"
manufacturer: "Some Name"
sensor:
- name: "Awning State"
state_topic: "awning_1/state"
qos: 0
unique_id: "awning01_state"
device:
identifiers: "awning_1"
name: "Awning State Sensor"
model: "ESP32-C6 RC Manager"
manufacturer: "Some Name"