Files
TX-Ultimate-Easy/ESPHome/TX-Ultimate-Easy-ESPHome_core_hw_buttons.yaml
2025-08-05 20:01:32 +02:00

410 lines
15 KiB
YAML

####################################################################################################
##### TX Ultimate Easy for ESPHome #####
##### Repository: https://github.com/edwardtfn/TX-Ultimate-Easy #####
####################################################################################################
##### Purpose: ESPHome Core - Hardware - Buttons #####
####################################################################################################
##### Author: edwardtfn - https://github.com/edwardtfn - https://buymeacoffee.com/edwardfirmo #####
####################################################################################################
##### NOTE: #####
##### - Make changes ONLY if absolutely necessary and you have the required knowledge. #####
##### - For normal system use, modifications to this file are NOT required. #####
####################################################################################################
---
substitutions:
invalid_cooldown: 100ms
BUTTON_ACTION_NONE_TEXT: "None"
BUTTON_1_ACTION_TEXT: "Relay 1 (toggle)"
BUTTON_2_ACTION_TEXT: "Relay 2 (toggle)"
BUTTON_3_ACTION_TEXT: "Relay 3 (toggle)"
BUTTON_4_ACTION_TEXT: "Relay 4 (toggle)"
BUTTON_1_ACTION_FAILSAFE_TEXT: "Relay 1 (API failsafe only)"
BUTTON_2_ACTION_FAILSAFE_TEXT: "Relay 2 (API failsafe only)"
BUTTON_3_ACTION_FAILSAFE_TEXT: "Relay 3 (API failsafe only)"
BUTTON_4_ACTION_FAILSAFE_TEXT: "Relay 4 (API failsafe only)"
BUTTON_CLICK_MIN_LENGTH: '50' # The minimum duration the click should last, in msec
BUTTON_CLICK_MAX_LENGTH: '350' # The maximum duration the click should last, in msec
BUTTON_MULTI_CLICK_DELAY: '250' # The time to wait for another click, in msec
BUTTON_PRESS_TIMEOUT: '10000' # Ignore if button is pressed for longer than this time, in msec
BUTTON_LONG_PRESS_DELAY: '800' # The time to wait to consider a long press, in msec
# Button numbers constants
BUTTON_1_ID: '1'
BUTTON_2_ID: '2'
BUTTON_3_ID: '3'
BUTTON_4_ID: '4'
TAG_CORE_HW_BUTTONS: core.hw.buttons
binary_sensor:
- &binary_sensor_button_base
id: bs_button_2
name: Button 2
icon: mdi:gesture-tap-box
platform: template
internal: true
- id: bs_button_3
name: Button 3
<<: *binary_sensor_button_base
- id: bs_button_4
name: Button 4
<<: *binary_sensor_button_base
- id: bs_button_1
name: Button 1
internal: false
<<: *binary_sensor_button_base
esphome:
platformio_options:
build_flags:
- -D TX_ULTIMATE_EASY_CORE_HW_BUTTONS
globals:
# Packed button actions - replaces 4 separate selects
- id: button_actions_packed
type: uint16_t
restore_value: true
initial_value: '4369' # Binary: 0001000100010001 = all buttons set to option 1 (default action)
# Bit layout: [button4(4)][button3(4)][button2(4)][button1(4)]
# Each button uses 4 bits: 0=None, 1=Toggle, 2=Failsafe, 3-15=Reserved for future options
- id: button_press_button
type: uint8_t
restore_value: false
initial_value: '0'
- id: button_press_position
type: uint8_t
restore_value: false
initial_value: '0'
- id: button_press_start_time
type: uint32_t
restore_value: false
initial_value: '0'
- id: click_counter
type: uint8_t
restore_value: false
initial_value: '0'
script:
- id: !extend boot_initialize
then:
- script.execute: boot_initialize_buttons
- id: boot_initialize_buttons
mode: restart
then:
- lambda: |-
bs_button_1->publish_state(false);
bs_button_1->set_internal(id(gang_count) < 1);
sl_button_1_action->set_internal(id(gang_count) < 1);
bs_button_2->publish_state(false);
bs_button_2->set_internal(id(gang_count) < 2);
sl_button_2_action->set_internal(id(gang_count) < 2);
bs_button_3->publish_state(false);
bs_button_3->set_internal(id(gang_count) < 3);
sl_button_3_action->set_internal(id(gang_count) < 3);
bs_button_4->publish_state(false);
bs_button_4->set_internal(id(gang_count) < 4);
sl_button_4_action->set_internal(id(gang_count) < 4);
- id: button_action
mode: parallel
parameters:
button: uint8_t
action: string
count: uint8_t
then:
- lambda: |-
// Send event to Home Assistant
esphome::api::CustomAPIDevice ha_event;
ha_event.fire_homeassistant_event("${EVENT_NAME}", {
{"device_name", id(tx_device_name).state.c_str()},
{"firmware", "${version}"},
{"domain", "touch"},
{"type", "button"},
{"action", action.c_str()},
{"button_id", std::to_string(button)},
{"count", std::to_string(count)},
{"position", std::to_string(id(button_press_position))}
});
ESP_LOGI("${TAG_CORE_HW_BUTTONS}", "Button %" PRIu8 " action: '%s'", button, action.c_str());
if (action == "click") {
switch (button) {
case ${BUTTON_1_ID}:
bs_button_1_click_event->publish_state(true);
break;
case ${BUTTON_2_ID}:
bs_button_2_click_event->publish_state(true);
break;
case ${BUTTON_3_ID}:
bs_button_3_click_event->publish_state(true);
break;
case ${BUTTON_4_ID}:
bs_button_4_click_event->publish_state(true);
break;
}
} else if (action == "double_click") {
switch (button) {
case ${BUTTON_1_ID}:
bs_button_1_double_click_event->publish_state(true);
break;
case ${BUTTON_2_ID}:
bs_button_2_double_click_event->publish_state(true);
break;
case ${BUTTON_3_ID}:
bs_button_3_double_click_event->publish_state(true);
break;
case ${BUTTON_4_ID}:
bs_button_4_double_click_event->publish_state(true);
break;
}
} else if (action == "long_press") {
switch (button) {
case ${BUTTON_1_ID}:
bs_button_1_long_press_event->publish_state(true);
break;
case ${BUTTON_2_ID}:
bs_button_2_long_press_event->publish_state(true);
break;
case ${BUTTON_3_ID}:
bs_button_3_long_press_event->publish_state(true);
break;
case ${BUTTON_4_ID}:
bs_button_4_long_press_event->publish_state(true);
break;
}
}
id(button_press_button) = 0;
id(button_press_position) = 0;
id(button_press_start_time) = 0;
id(click_counter) = 0;
buttons_release->execute();
- id: button_click_event
mode: restart
parameters:
button: uint8_t
click_count: uint8_t
then:
- delay:
milliseconds: ${BUTTON_MULTI_CLICK_DELAY}
- lambda: |-
std::string event_name;
if (click_count == 1) event_name = "click";
else if (click_count == 2) event_name = "double_click";
else event_name = "multiple_click";
button_action->execute(button, event_name.c_str(), click_count);
- id: buttons_release
mode: restart
then:
- lambda: |-
std::vector<binary_sensor::BinarySensor*> buttons = {
bs_button_1, bs_button_2, bs_button_3, bs_button_4
};
for (auto* button : buttons) {
if (button->state) {
button->publish_state(false);
}
}
- id: !extend dump_config
then:
- lambda: |-
std::string gang_count_plural_suffix = id(gang_count) > 1 ? "s" : "";
// Button's actions
ESP_LOGCONFIG("${TAG_CORE_HW_BUTTONS}", "Button%s action%s:",
gang_count_plural_suffix.c_str(),
gang_count_plural_suffix.c_str());
ESP_LOGCONFIG("${TAG_CORE_HW_BUTTONS}", " Relay 1: %s", sl_button_1_action->has_state() ?
sl_button_1_action->state.c_str() : "Unknown");
if (id(gang_count) >= 2)
ESP_LOGCONFIG("${TAG_CORE_HW_BUTTONS}", " Relay 2: %s", sl_button_2_action->has_state() ?
sl_button_2_action->state.c_str() : "Unknown");
if (id(gang_count) >= 3)
ESP_LOGCONFIG("${TAG_CORE_HW_BUTTONS}", " Relay 3: %s", sl_button_3_action->has_state() ?
sl_button_3_action->state.c_str() : "Unknown");
if (id(gang_count) >= 4)
ESP_LOGCONFIG("${TAG_CORE_HW_BUTTONS}", " Relay 4: %s", sl_button_4_action->has_state() ?
sl_button_4_action->state.c_str() : "Unknown");
- id: !extend dump_config_list_packages
then:
- script.wait: dump_config
- lambda: |-
// Check for requirements
#if !defined(TX_ULTIMATE_EASY_CORE_COMMON)
#error "The package TX-Ultimate-Easy-ESPHome_core_common.yaml is required."
#endif
// Identify itself
ESP_LOGCONFIG(ESPHOME_PROJECT_NAME, " - Core - Hardware - Buttons");
- id: !extend restore_from_nvs
then:
- select.set_index:
id: sl_button_1_action
index: !lambda return std::min((uint8_t)(id(button_actions_packed) & 0x0F), (uint8_t)2);
- select.set_index:
id: sl_button_2_action
index: !lambda return std::min((uint8_t)((id(button_actions_packed) >> 4) & 0x0F), (uint8_t)2);
- select.set_index:
id: sl_button_3_action
index: !lambda return std::min((uint8_t)((id(button_actions_packed) >> 8) & 0x0F), (uint8_t)2);
- select.set_index:
id: sl_button_4_action
index: !lambda return std::min((uint8_t)((id(button_actions_packed) >> 12) & 0x0F), (uint8_t)2);
- id: !extend touch_on_multi_touch_release
then:
- script.execute: buttons_release
- id: !extend touch_on_press
then:
- script.execute:
id: touch_on_press_buttons
button: !lambda return button;
position: !lambda return position;
- id: touch_on_press_buttons
mode: restart
parameters:
button: uint8_t
position: uint8_t
then:
- lambda: |-
id(button_press_start_time) = millis();
id(button_press_button) = button;
// Update counters
if (id(button_press_position) == position) {
id(click_counter)++;
} else {
id(click_counter) = 1;
id(button_press_position) = position;
}
// Update binary sensor
switch (button) {
case 1:
bs_button_1->publish_state(true);
break;
case 2:
bs_button_2->publish_state(true);
break;
case 3:
bs_button_3->publish_state(true);
break;
case 4:
bs_button_4->publish_state(true);
break;
}
- id: !extend touch_on_release
then:
- lambda: |-
uint32_t current_time = millis();
buttons_release->execute();
if (id(button_press_start_time) > 0 and
id(button_press_start_time) < current_time) {
uint32_t press_duration = current_time - id(button_press_start_time);
// Handle overflow (optional, since it's unlikely to happen here)
ESP_LOGI("${TAG_CORE_HW_BUTTONS}", "Button press duration: %" PRIu32 " ms", press_duration);
if (press_duration < ${BUTTON_CLICK_MIN_LENGTH}) {
ESP_LOGW("${TAG_CORE_HW_BUTTONS}", "Ignoring button press (too short)");
} else if (press_duration >= ${BUTTON_CLICK_MIN_LENGTH} and
press_duration <= ${BUTTON_CLICK_MAX_LENGTH}) { // Short/normal click
button_click_event->execute(id(button_press_button), id(click_counter));
} else if (press_duration >= ${BUTTON_LONG_PRESS_DELAY} and press_duration <= ${BUTTON_PRESS_TIMEOUT}) {
button_action->execute(id(button_press_button), "long_press", 1);
} else if (press_duration > ${BUTTON_PRESS_TIMEOUT}) { // Timeout or invalid
ESP_LOGW("${TAG_CORE_HW_BUTTONS}",
"Button press cancelled or timed out after ${BUTTON_PRESS_TIMEOUT} ms");
}
} else {
ESP_LOGW("${TAG_CORE_HW_BUTTONS}", "Press event timestamp not recorded yet");
}
id(button_press_start_time) = 0;
- id: !extend touch_swipe_left
then:
- script.execute: buttons_release
- id: !extend touch_swipe_right
then:
- script.execute: buttons_release
select:
- &button_select_action_base
id: sl_button_1_action
name: Button 1 action
platform: template
options:
- "${BUTTON_ACTION_NONE_TEXT}"
- "${BUTTON_1_ACTION_TEXT}"
- "${BUTTON_1_ACTION_FAILSAFE_TEXT}"
initial_option: "${BUTTON_1_ACTION_TEXT}"
optimistic: true
restore_value: false
internal: true
entity_category: config
disabled_by_default: false
icon: mdi:dip-switch
on_value:
then:
- lambda: |-
id(button_actions_packed) = (id(button_actions_packed) & ~(0x0F << 0)) |
((id(sl_button_1_action).active_index().value_or(1) & 0x0F) << 0);
- id: sl_button_2_action
name: Button 2 action
platform: template
options:
- "${BUTTON_ACTION_NONE_TEXT}"
- "${BUTTON_2_ACTION_TEXT}"
- "${BUTTON_2_ACTION_FAILSAFE_TEXT}"
initial_option: "${BUTTON_2_ACTION_TEXT}"
<<: *button_select_action_base
on_value:
then:
- lambda: |-
id(button_actions_packed) = (id(button_actions_packed) & ~(0x0F << 4)) |
((id(sl_button_2_action).active_index().value_or(1) & 0x0F) << 4);
- id: sl_button_3_action
name: Button 3 action
platform: template
options:
- "${BUTTON_ACTION_NONE_TEXT}"
- "${BUTTON_3_ACTION_TEXT}"
- "${BUTTON_3_ACTION_FAILSAFE_TEXT}"
initial_option: "${BUTTON_3_ACTION_TEXT}"
<<: *button_select_action_base
on_value:
then:
- lambda: |-
id(button_actions_packed) = (id(button_actions_packed) & ~(0x0F << 8)) |
((id(sl_button_3_action).active_index().value_or(1) & 0x0F) << 8);
- id: sl_button_4_action
name: Button 4 action
platform: template
options:
- "${BUTTON_ACTION_NONE_TEXT}"
- "${BUTTON_4_ACTION_TEXT}"
- "${BUTTON_4_ACTION_FAILSAFE_TEXT}"
initial_option: "${BUTTON_4_ACTION_TEXT}"
<<: *button_select_action_base
on_value:
then:
- lambda: |-
id(button_actions_packed) = (id(button_actions_packed) & ~(0x0F << 12)) |
((id(sl_button_4_action).active_index().value_or(1) & 0x0F) << 12);
...