@@ -77,6 +77,14 @@ globals:
7777 - { id: b2_consumed, type: bool, restore_value: no, initial_value: 'false' }
7878 - { id: b1_tap_count, type: int, restore_value: no, initial_value: '0' }
7979 - { id: b1_last_tap_ms, type: uint32_t, restore_value: no, initial_value: '0' }
80+ # Button 2 gets its own tap counter too: with "Swap Buttons" ON, Button 2 is the
81+ # close button, so it -- not Button 1 -- carries the 7-tap homing gesture. Only
82+ # whichever button is currently the close button ever increments its counter.
83+ - { id: b2_tap_count, type: int, restore_value: no, initial_value: '0' }
84+ - { id: b2_last_tap_ms, type: uint32_t, restore_value: no, initial_value: '0' }
85+ # Persisted button-swap state. Like global_stepper_direction, it is the single
86+ # source of truth behind a stateless "Swap Buttons" select (see the select block).
87+ - { id: global_btn_swap, type: bool, restore_value: yes, initial_value: 'false' }
8088
8189# --- Stall handling (homing) ------------------------------------------------
8290# The Ropener homes sensorlessly: drive at the closed end until StallGuard fires,
@@ -225,6 +233,10 @@ script:
225233 - logger.log : " Home position set to 0"
226234
227235 # Hold-to-run engage (armed on press, fired after hold_run_ms).
236+ # Hold-to-run for each button. The MOTION depends on the button's current role
237+ # (set by the "Swap Buttons" select): the close button drives close, the open
238+ # button drives open. Button 1 is the close button when swap is OFF; Button 2 is
239+ # the close button when swap is ON.
228240 - id : b1_hold_check
229241 mode : restart
230242 then :
@@ -235,22 +247,36 @@ script:
235247 then :
236248 - globals.set : { id: b1_hold_active, value: "true" }
237249 - globals.set : { id: b1_consumed, value: "true" }
238- - cover.close : ropener_cover
239- - logger.log : " Button 1 held: closing while held"
250+ - if :
251+ condition : { lambda: 'return !id(global_btn_swap);' }
252+ then :
253+ - cover.close : ropener_cover
254+ - logger.log : " Button 1 held: closing while held"
255+ else :
256+ - cover.open : ropener_cover
257+ - logger.log : " Button 1 held: opening while held"
240258 - id : b2_hold_check
241259 mode : restart
242260 then :
243261 - delay : ${hold_run_ms}ms
244262 - if :
245263 condition :
246- lambda : ' return id(b2_down) && !id(b1_down) && id(global_state) == 0;'
264+ lambda : ' return id(b2_down) && !id(b1_down) && id(global_state) == 0 && id(b2_tap_count) < 7 ;'
247265 then :
248266 - globals.set : { id: b2_hold_active, value: "true" }
249267 - globals.set : { id: b2_consumed, value: "true" }
250- - cover.open : ropener_cover
251- - logger.log : " Button 2 held: opening while held"
268+ - if :
269+ condition : { lambda: 'return id(global_btn_swap);' }
270+ then :
271+ - cover.close : ropener_cover
272+ - logger.log : " Button 2 held: closing while held"
273+ else :
274+ - cover.open : ropener_cover
275+ - logger.log : " Button 2 held: opening while held"
252276
253- # Single-tap close for Button 1 (deferred so a 7-tap burst can chain).
277+ # Deferred single-tap CLOSE, one per button. Only the current close button ever
278+ # schedules its commit (the open button opens immediately on release); the delay
279+ # lets a 7-tap homing burst chain without the first tap committing a close.
254280 - id : b1_tap_commit
255281 mode : restart
256282 then :
@@ -262,6 +288,17 @@ script:
262288 - cover.close : ropener_cover
263289 - logger.log : " Button 1 tap: closing"
264290 - globals.set : { id: b1_tap_count, value: "0" }
291+ - id : b2_tap_commit
292+ mode : restart
293+ then :
294+ - delay : ${tap_window_ms}ms
295+ - if :
296+ condition :
297+ lambda : ' return id(global_state) == 0 && id(b2_tap_count) < 7;'
298+ then :
299+ - cover.close : ropener_cover
300+ - logger.log : " Button 2 tap: closing"
301+ - globals.set : { id: b2_tap_count, value: "0" }
265302
266303 # Two-button "set home" combo.
267304 - id : combo_home
@@ -325,29 +362,40 @@ binary_sensor:
325362 - script.stop : b1_hold_check
326363 - script.stop : b2_hold_check
327364 - script.stop : b1_tap_commit
365+ - script.stop : b2_tap_commit
328366 - cover.stop : ropener_cover
329367 - globals.set : { id: b1_hold_active, value: "false" }
330368 - globals.set : { id: b2_hold_active, value: "false" }
331369 - globals.set : { id: b1_consumed, value: "true" }
332370 - globals.set : { id: b2_consumed, value: "true" }
333371 - script.execute : combo_home
334372 else :
335- - script.stop : b1_tap_commit
336- - lambda : |-
337- uint32_t now = millis();
338- if (now - id(b1_last_tap_ms) > ${tap_window_ms}) id(b1_tap_count) = 0;
339- id(b1_tap_count) += 1;
340- id(b1_last_tap_ms) = now;
341- - globals.set : { id: b1_consumed, value: "false" }
373+ # Idle, single button. Behaviour depends on Button 1's role.
342374 - if :
343- condition : { lambda: 'return id(b1_tap_count) >= 7 ;' }
375+ condition : { lambda: 'return ! id(global_btn_swap) ;' }
344376 then :
345- - script.stop : b1_hold_check
346- - globals.set : { id: b1_tap_count, value: "0" }
347- - globals.set : { id: b1_consumed, value: "true" }
348- - script.execute : home_start
349- - logger.log : " Button 1 x7: starting homing"
377+ # CLOSE button: count taps (7 -> homing), defer the close.
378+ - script.stop : b1_tap_commit
379+ - lambda : |-
380+ uint32_t now = millis();
381+ if (now - id(b1_last_tap_ms) > ${tap_window_ms}) id(b1_tap_count) = 0;
382+ id(b1_tap_count) += 1;
383+ id(b1_last_tap_ms) = now;
384+ - globals.set : { id: b1_consumed, value: "false" }
385+ - if :
386+ condition : { lambda: 'return id(b1_tap_count) >= 7;' }
387+ then :
388+ - script.stop : b1_hold_check
389+ - globals.set : { id: b1_tap_count, value: "0" }
390+ - globals.set : { id: b1_consumed, value: "true" }
391+ - script.execute : home_start
392+ - logger.log : " Button 1 x7: starting homing"
393+ else :
394+ - script.execute : b1_hold_check
350395 else :
396+ # OPEN button: no homing gesture; open immediately on release.
397+ - globals.set : { id: b1_tap_count, value: "0" }
398+ - globals.set : { id: b1_consumed, value: "false" }
351399 - script.execute : b1_hold_check
352400 on_release :
353401 - globals.set : { id: b1_down, value: "false" }
@@ -366,7 +414,15 @@ binary_sensor:
366414 then :
367415 - globals.set : { id: b1_consumed, value: "false" }
368416 else :
369- - script.execute : b1_tap_commit
417+ # Tap: the close button defers (allows 7-tap chaining); the open
418+ # button opens immediately.
419+ - if :
420+ condition : { lambda: 'return !id(global_btn_swap);' }
421+ then :
422+ - script.execute : b1_tap_commit
423+ else :
424+ - cover.open : ropener_cover
425+ - logger.log : " Button 1 tap: opening"
370426
371427 - id : !extend btn2
372428 on_press :
@@ -375,33 +431,64 @@ binary_sensor:
375431 condition : { lambda: 'return id(global_state) == 3;' }
376432 then :
377433 - script.stop : b2_hold_check
434+ - script.stop : b2_tap_commit
378435 - script.execute : set_home_zero
379436 - globals.set : { id: b2_consumed, value: "true" }
437+ - globals.set : { id: b2_tap_count, value: "0" }
380438 - logger.log : " Homing cancelled (Button 2)"
381439 else :
382440 - if :
383441 condition : { lambda: 'return id(global_state) == 1 || id(global_state) == 2;' }
384442 then :
385443 - script.stop : b2_hold_check
444+ - script.stop : b2_tap_commit
386445 - cover.stop : ropener_cover
387446 - globals.set : { id: b2_consumed, value: "true" }
447+ - globals.set : { id: b2_tap_count, value: "0" }
388448 else :
389449 - if :
390450 condition : { lambda: 'return id(b1_down);' }
391451 then :
392452 - script.stop : b1_hold_check
393453 - script.stop : b2_hold_check
394454 - script.stop : b1_tap_commit
455+ - script.stop : b2_tap_commit
395456 - cover.stop : ropener_cover
396457 - globals.set : { id: b1_hold_active, value: "false" }
397458 - globals.set : { id: b2_hold_active, value: "false" }
398459 - globals.set : { id: b1_consumed, value: "true" }
399460 - globals.set : { id: b2_consumed, value: "true" }
400461 - globals.set : { id: b1_tap_count, value: "0" }
462+ - globals.set : { id: b2_tap_count, value: "0" }
401463 - script.execute : combo_home
402464 else :
403- - globals.set : { id: b2_consumed, value: "false" }
404- - script.execute : b2_hold_check
465+ # Idle, single button. Behaviour depends on Button 2's role.
466+ - if :
467+ condition : { lambda: 'return id(global_btn_swap);' }
468+ then :
469+ # CLOSE button (swap ON): count taps (7 -> homing), defer close.
470+ - script.stop : b2_tap_commit
471+ - lambda : |-
472+ uint32_t now = millis();
473+ if (now - id(b2_last_tap_ms) > ${tap_window_ms}) id(b2_tap_count) = 0;
474+ id(b2_tap_count) += 1;
475+ id(b2_last_tap_ms) = now;
476+ - globals.set : { id: b2_consumed, value: "false" }
477+ - if :
478+ condition : { lambda: 'return id(b2_tap_count) >= 7;' }
479+ then :
480+ - script.stop : b2_hold_check
481+ - globals.set : { id: b2_tap_count, value: "0" }
482+ - globals.set : { id: b2_consumed, value: "true" }
483+ - script.execute : home_start
484+ - logger.log : " Button 2 x7: starting homing"
485+ else :
486+ - script.execute : b2_hold_check
487+ else :
488+ # OPEN button: no homing gesture; open immediately on release.
489+ - globals.set : { id: b2_tap_count, value: "0" }
490+ - globals.set : { id: b2_consumed, value: "false" }
491+ - script.execute : b2_hold_check
405492 on_release :
406493 - globals.set : { id: b2_down, value: "false" }
407494 - script.stop : b2_hold_check
@@ -419,8 +506,15 @@ binary_sensor:
419506 then :
420507 - globals.set : { id: b2_consumed, value: "false" }
421508 else :
422- - cover.open : ropener_cover
423- - logger.log : " Button 2 tap: opening"
509+ # Tap: the close button defers (allows 7-tap chaining); the open
510+ # button opens immediately.
511+ - if :
512+ condition : { lambda: 'return id(global_btn_swap);' }
513+ then :
514+ - script.execute : b2_tap_commit
515+ else :
516+ - cover.open : ropener_cover
517+ - logger.log : " Button 2 tap: opening"
424518
425519 # Rename core's "Button 3 (Wi-Fi reset)" to Ropener's "WiFi Reset" and attach
426520 # the hold-to-erase-credentials action.
@@ -490,6 +584,36 @@ select:
490584 global_speed->update();
491585 global_preferences->sync();
492586
587+ # Swap the two physical buttons' open/close roles -- same select UI as Motor
588+ # Direction, and its sibling install-orientation setting (sits right under it in
589+ # Setup). "Normal" = Button 1 closes, Button 2 opens (the shipped layout).
590+ # "Swapped" = Button 1 opens, Button 2 closes -- for a mirror-image install (e.g.
591+ # pull-rope on the left) where the flipped unit makes the buttons feel reversed.
592+ # The 7-tap homing gesture and the deferred single-tap always follow the CLOSE
593+ # button. Like Motor Direction it holds NO state of its own: it reads and writes
594+ # the persisted global_btn_swap, so it never fights the restored value. Only
595+ # remaps the on-device buttons; the web UI, Home Assistant, and the schedule
596+ # open/close are unaffected. (A left-side install usually wants Motor Direction =
597+ # Reversed too.)
598+ - platform : template
599+ name : " Swap Buttons"
600+ id : sel_btn_swap
601+ icon : " mdi:swap-horizontal"
602+ entity_category : CONFIG
603+ web_server : { sorting_weight: 2, sorting_group_id: group_setup }
604+ update_interval : 1s
605+ options :
606+ - " Normal"
607+ - " Swapped ↔"
608+ lambda : |-
609+ if (id(global_btn_swap)) return std::string("Swapped ↔");
610+ return std::string("Normal");
611+ set_action :
612+ - lambda : |-
613+ id(global_btn_swap) = (x.find("Swap") != std::string::npos);
614+ global_btn_swap->update();
615+ global_preferences->sync();
616+
493617# --- Rename core tuning entities to Ropener's existing entity_ids -------------
494618number :
495619 # Raw IRUN (register 1-31) is meaningless to users -- the friendly "Motor
0 commit comments