@@ -82,6 +82,9 @@ globals:
8282 # whichever button is currently the close button ever increments its counter.
8383 - { id: b2_tap_count, type: int, restore_value: no, initial_value: '0' }
8484 - { 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' }
8588
8689# --- Stall handling (homing) ------------------------------------------------
8790# The Ropener homes sensorlessly: drive at the closed end until StallGuard fires,
@@ -231,7 +234,7 @@ script:
231234
232235 # Hold-to-run engage (armed on press, fired after hold_run_ms).
233236 # Hold-to-run for each button. The MOTION depends on the button's current role
234- # (set by the "Swap Buttons" switch ): the close button drives close, the open
237+ # (set by the "Swap Buttons" select ): the close button drives close, the open
235238 # button drives open. Button 1 is the close button when swap is OFF; Button 2 is
236239 # the close button when swap is ON.
237240 - id : b1_hold_check
@@ -245,7 +248,7 @@ script:
245248 - globals.set : { id: b1_hold_active, value: "true" }
246249 - globals.set : { id: b1_consumed, value: "true" }
247250 - if :
248- condition : { lambda: 'return !id(btn_swap).state ;' }
251+ condition : { lambda: 'return !id(global_btn_swap) ;' }
249252 then :
250253 - cover.close : ropener_cover
251254 - logger.log : " Button 1 held: closing while held"
@@ -263,7 +266,7 @@ script:
263266 - globals.set : { id: b2_hold_active, value: "true" }
264267 - globals.set : { id: b2_consumed, value: "true" }
265268 - if :
266- condition : { lambda: 'return id(btn_swap).state ;' }
269+ condition : { lambda: 'return id(global_btn_swap) ;' }
267270 then :
268271 - cover.close : ropener_cover
269272 - logger.log : " Button 2 held: closing while held"
@@ -369,7 +372,7 @@ binary_sensor:
369372 else :
370373 # Idle, single button. Behaviour depends on Button 1's role.
371374 - if :
372- condition : { lambda: 'return !id(btn_swap).state ;' }
375+ condition : { lambda: 'return !id(global_btn_swap) ;' }
373376 then :
374377 # CLOSE button: count taps (7 -> homing), defer the close.
375378 - script.stop : b1_tap_commit
@@ -414,7 +417,7 @@ binary_sensor:
414417 # Tap: the close button defers (allows 7-tap chaining); the open
415418 # button opens immediately.
416419 - if :
417- condition : { lambda: 'return !id(btn_swap).state ;' }
420+ condition : { lambda: 'return !id(global_btn_swap) ;' }
418421 then :
419422 - script.execute : b1_tap_commit
420423 else :
@@ -461,7 +464,7 @@ binary_sensor:
461464 else :
462465 # Idle, single button. Behaviour depends on Button 2's role.
463466 - if :
464- condition : { lambda: 'return id(btn_swap).state ;' }
467+ condition : { lambda: 'return id(global_btn_swap) ;' }
465468 then :
466469 # CLOSE button (swap ON): count taps (7 -> homing), defer close.
467470 - script.stop : b2_tap_commit
@@ -506,7 +509,7 @@ binary_sensor:
506509 # Tap: the close button defers (allows 7-tap chaining); the open
507510 # button opens immediately.
508511 - if :
509- condition : { lambda: 'return id(btn_swap).state ;' }
512+ condition : { lambda: 'return id(global_btn_swap) ;' }
510513 then :
511514 - script.execute : b2_tap_commit
512515 else :
@@ -581,6 +584,36 @@ select:
581584 global_speed->update();
582585 global_preferences->sync();
583586
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+
584617# --- Rename core tuning entities to Ropener's existing entity_ids -------------
585618number :
586619 # Raw IRUN (register 1-31) is meaningless to users -- the friendly "Motor
@@ -743,24 +776,6 @@ switch:
743776 optimistic : true
744777 restore_mode : RESTORE_DEFAULT_OFF
745778
746- # Swap the two physical buttons' open/close roles. Default OFF = Button 1 closes,
747- # Button 2 opens (the shipped layout). ON = Button 1 opens, Button 2 closes -- for
748- # a mirror-image install (e.g. the pull-rope on the left instead of the right)
749- # where the unit sits flipped and the buttons otherwise feel reversed. The 7-tap
750- # homing gesture and the deferred single-tap always follow the CLOSE button, so
751- # homing lives on whichever button closes. This only remaps the on-device buttons;
752- # the web UI, Home Assistant, and the schedule open/close are unaffected. (Motor
753- # Direction, a separate setting, reverses the motor itself -- a left-side install
754- # usually wants both.)
755- - platform : template
756- name : " Swap Buttons"
757- id : btn_swap
758- icon : " mdi:swap-horizontal"
759- entity_category : CONFIG
760- web_server : { sorting_weight: 3, sorting_group_id: group_setup }
761- optimistic : true
762- restore_mode : RESTORE_DEFAULT_OFF
763-
764779text_sensor :
765780 # Surfaces StallGuard's arming state, which is otherwise invisible. The
766781 # failure it exists to catch: if TCOOLTHRS is below the cruise TSTEP for the
0 commit comments