@@ -24,17 +24,15 @@ substitutions:
2424 BUTTON_3_ACTION_FAILSAFE_TEXT : " Relay 3 (API failsafe only)"
2525 BUTTON_4_ACTION_FAILSAFE_TEXT : " Relay 4 (API failsafe only)"
2626
27- BUTTON_CLICK_MIN_LENGTH : ' 50' # The minimum duration the click should last, in msec
28- BUTTON_CLICK_MAX_LENGTH : ' 350' # The maximum duration the click should last, in msec
29- BUTTON_MULTI_CLICK_DELAY : ' 250' # The time to wait for another click, in msec
30- BUTTON_PRESS_TIMEOUT : ' 10000' # Ignore if button is pressed for longer than this time, in msec
31- BUTTON_LONG_PRESS_DELAY : ' 800' # The time to wait to consider a long press, in msec
27+ BUTTON_MULTI_CLICK_DELAY : " 250" # The time to wait for another click, in msec
28+ BUTTON_PRESS_TIMEOUT : " 10000" # Ignore if button is pressed for longer than this time, in msec
29+ BUTTON_LONG_PRESS_DELAY : " 500" # The time to wait to consider a long press, in msec
3230
3331 # Button numbers constants
34- BUTTON_1_ID : ' 1 '
35- BUTTON_2_ID : ' 2 '
36- BUTTON_3_ID : ' 3 '
37- BUTTON_4_ID : ' 4 '
32+ BUTTON_1_ID : " 1 "
33+ BUTTON_2_ID : " 2 "
34+ BUTTON_3_ID : " 3 "
35+ BUTTON_4_ID : " 4 "
3836
3937 TAG_HW_BUTTONS : tx_ultimate_easy.hw.buttons
4038
@@ -69,29 +67,36 @@ globals:
6967 - id : button_actions_packed
7068 type : uint16_t
7169 restore_value : true
72- initial_value : ' 4369' # Binary: 0001000100010001 = all buttons set to option 1 (default action)
70+ initial_value : " 4369" # Binary: 0001000100010001 = all buttons set to option 1 (default action)
7371 # Bit layout: [button4(4)][button3(4)][button2(4)][button1(4)]
7472 # Each button uses 4 bits: 0=None, 1=Toggle, 2=Failsafe, 3-15=Reserved for future options
7573
7674 - id : button_press_button
7775 type : uint8_t
7876 restore_value : false
79- initial_value : ' 0 '
77+ initial_value : " 0 "
8078
8179 - id : button_press_position
8280 type : uint8_t
8381 restore_value : false
84- initial_value : ' 0 '
82+ initial_value : " 0 "
8583
8684 - id : button_press_start_time
8785 type : uint32_t
8886 restore_value : false
89- initial_value : ' 0'
87+ initial_value : " 0"
88+ # Used for: current press duration (press → release)
89+
90+ - id : last_release_time
91+ type : uint32_t
92+ restore_value : false
93+ initial_value : " 0"
94+ # Used for: inter-click gap timing (time since last release)
9095
9196 - id : click_counter
9297 type : uint8_t
9398 restore_value : false
94- initial_value : ' 0 '
99+ initial_value : " 0 "
95100
96101script :
97102 - id : !extend boot_initialize
@@ -180,6 +185,7 @@ script:
180185 id(button_press_button) = 0;
181186 id(button_press_position) = 0;
182187 id(button_press_start_time) = 0;
188+ id(last_release_time) = 0;
183189 id(click_counter) = 0;
184190 buttons_release->execute();
185191
@@ -210,6 +216,18 @@ script:
210216 }
211217 }
212218
219+ - id : buttons_reset_state
220+ mode : restart
221+ then :
222+ - lambda : |-
223+ // Reset all button-related global state
224+ id(button_press_start_time) = 0;
225+ id(last_release_time) = 0;
226+ id(click_counter) = 0;
227+ id(button_press_button) = 0;
228+ id(button_press_position) = 0;
229+ buttons_release->execute();
230+
213231 - id : !extend dump_config
214232 then :
215233 - lambda : |-
@@ -275,7 +293,7 @@ script:
275293
276294 - id : !extend touch_on_multi_touch_release
277295 then :
278- - script.execute : buttons_release
296+ - script.execute : buttons_reset_state
279297
280298 - id : !extend touch_on_press
281299 then :
@@ -291,15 +309,46 @@ script:
291309 position : uint8_t
292310 then :
293311 - lambda : |-
294- id(button_press_start_time) = millis();
295- id(button_press_button) = button;
296- // Update counters
297- if (id(button_press_position) == position) {
298- id(click_counter)++;
299- } else {
312+ uint32_t current_time = millis();
313+ // Check if we should start a new click sequence:
314+ // 1. Previous cycle completed (start_time was reset to 0)
315+ // 2. Position changed
316+ // 3. Enough time has passed since last release (more than multi-click delay)
317+ bool start_new_sequence = false;
318+ if (id(button_press_start_time) == 0) {
319+ // Previous cycle completed
320+ start_new_sequence = true;
321+ } else if (id(button_press_position) != position) {
322+ // Position changed
323+ start_new_sequence = true;
324+ } else if (id(last_release_time) > 0) {
325+ // Check if enough time has passed since last release (for inter-click gap)
326+ // Use runtime value from HA instead of compile-time constant
327+ uint32_t gap_window = (uint32_t) nr_button_multi_click_delay->state;
328+ uint32_t time_since_last_release;
329+ // Handle millis() overflow (wraps after ~49 days)
330+ if (current_time >= id(last_release_time)) {
331+ time_since_last_release = current_time - id(last_release_time);
332+ } else {
333+ // Overflow occurred, calculate correctly
334+ time_since_last_release = (UINT32_MAX - id(last_release_time)) + current_time + 1;
335+ }
336+ if (time_since_last_release > gap_window) {
337+ // Too much time passed since last release, start fresh
338+ start_new_sequence = true;
339+ }
340+ }
341+
342+ if (start_new_sequence) {
300343 id(click_counter) = 1;
301344 id(button_press_position) = position;
345+ } else {
346+ // Same position and within multi-click window, increment counter
347+ id(click_counter)++;
302348 }
349+ // Set press start time for this press (for duration calculation)
350+ id(button_press_start_time) = current_time;
351+ id(button_press_button) = button;
303352 // Update binary sensor
304353 switch (button) {
305354 case 1:
@@ -321,34 +370,64 @@ script:
321370 - lambda : |-
322371 uint32_t current_time = millis();
323372 buttons_release->execute();
324- if (id(button_press_start_time) > 0 and
325- id(button_press_start_time) < current_time) {
326- uint32_t press_duration = current_time - id(button_press_start_time);
327- // Handle overflow (optional, since it's unlikely to happen here)
373+ if (id(button_press_start_time) > 0) {
374+ uint32_t press_duration;
375+ // Handle millis() overflow (wraps after ~49 days)
376+ if (current_time >= id(button_press_start_time)) {
377+ press_duration = current_time - id(button_press_start_time);
378+ } else {
379+ // Overflow occurred, calculate correctly
380+ press_duration = (UINT32_MAX - id(button_press_start_time)) + current_time + 1;
381+ }
328382 ESP_LOGI("${TAG_HW_BUTTONS}", "Button press duration: %" PRIu32 " ms", press_duration);
329- if (press_duration < ${BUTTON_CLICK_MIN_LENGTH}) {
330- ESP_LOGW("${TAG_HW_BUTTONS}", "Ignoring button press (too short)");
331- } else if (press_duration >= ${BUTTON_CLICK_MIN_LENGTH} and
332- press_duration <= ${BUTTON_CLICK_MAX_LENGTH}) { // Short/normal click
333- button_click_event->execute(id(button_press_button), id(click_counter));
334- } else if (press_duration >= ${BUTTON_LONG_PRESS_DELAY} and press_duration <= ${BUTTON_PRESS_TIMEOUT}) {
335- button_action->execute(id(button_press_button), "long_press", 1);
383+ // Update last release time for inter-click gap detection
384+ id(last_release_time) = current_time;
385+ if (press_duration <= ${BUTTON_LONG_PRESS_DELAY}) { // Short/normal click
386+ // Validate button is still valid (in case swipe/multi-touch happened)
387+ if (id(button_press_button) > 0 && id(button_press_button) <= 4) {
388+ // Don't reset start_time here - let button_action reset it after multi-click delay
389+ // This allows subsequent presses during the delay to increment the counter
390+ button_click_event->execute(id(button_press_button), id(click_counter));
391+ } else {
392+ ESP_LOGW("${TAG_HW_BUTTONS}", "Invalid button state, resetting");
393+ id(button_press_start_time) = 0;
394+ id(last_release_time) = 0;
395+ id(click_counter) = 0;
396+ id(button_press_button) = 0;
397+ }
398+ } else if (press_duration > ${BUTTON_LONG_PRESS_DELAY} and press_duration <= ${BUTTON_PRESS_TIMEOUT}) {
399+ // Long press: anything longer than BUTTON_LONG_PRESS_DELAY (and within timeout) is a long press
400+ // Validate button is still valid (in case swipe/multi-touch happened)
401+ if (id(button_press_button) > 0 && id(button_press_button) <= 4) {
402+ button_action->execute(id(button_press_button), "long_press", 1);
403+ } else {
404+ ESP_LOGW("${TAG_HW_BUTTONS}", "Invalid button state during long press, resetting");
405+ id(button_press_start_time) = 0;
406+ id(last_release_time) = 0;
407+ id(click_counter) = 0;
408+ id(button_press_button) = 0;
409+ }
336410 } else if (press_duration > ${BUTTON_PRESS_TIMEOUT}) { // Timeout or invalid
337411 ESP_LOGW("${TAG_HW_BUTTONS}",
338412 "Button press cancelled or timed out after ${BUTTON_PRESS_TIMEOUT} ms");
413+ id(button_press_start_time) = 0;
414+ id(last_release_time) = 0;
415+ id(click_counter) = 0;
339416 }
340417 } else {
341418 ESP_LOGW("${TAG_HW_BUTTONS}", "Press event timestamp not recorded yet");
419+ id(button_press_start_time) = 0;
420+ id(last_release_time) = 0;
421+ id(click_counter) = 0;
342422 }
343- id(button_press_start_time) = 0;
344423
345424 - id : !extend touch_swipe_${ID_SWIPE_LEFT_OR_UP_TEXT}
346425 then :
347- - script.execute : buttons_release
426+ - script.execute : buttons_reset_state
348427
349428 - id : !extend touch_swipe_${ID_SWIPE_RIGHT_OR_DOWN_TEXT}
350429 then :
351- - script.execute : buttons_release
430+ - script.execute : buttons_reset_state
352431
353432select :
354433 - &button_select_action_base
@@ -416,4 +495,3 @@ select:
416495 - lambda : |-
417496 id(button_actions_packed) = (id(button_actions_packed) & ~(0x0F << 12)) |
418497 ((id(sl_button_4_action).active_index().value_or(1) & 0x0F) << 12);
419- ...
0 commit comments