Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cover single button control #1268

Closed
muxa opened this issue Jun 19, 2021 · 24 comments
Closed

Cover single button control #1268

muxa opened this issue Jun 19, 2021 · 24 comments

Comments

@muxa
Copy link

muxa commented Jun 19, 2021

Describe the problem you have/What new integration you would like
I have a garage door that's controlled with a single button (a typical garage door control): press to start/stop, press again to move in opposite direction). The button is on a ESP8266 WiFi wall switch with a relay that shorts the original garage door button to control it remotely. This part was easy to connect.

I wanted to have a reliable way to control the cover with the single button as well as from Home Assistant, so that the state of the cover is always correct.

Please describe your use case for this integration and alternatives you've tried:
I have implemented this using a cover with template platform with quite a bit of C code that implements a state machine. I've also added a contact sensor to detect when the garage door is fully closed.
Here's the state machine diagram:

Garage Door State Machine Diagram

Here is the configuration and the code that I've got so far: https://gist.github.com/muxa/9d0b1be8ea7c3daed5a0d4f0db058e4f
It's working pretty well.

Additional context
What I'd like to do is to create a component to encapsulate this functionality (this will be a learning curve for me), publish it as an external component and eventually perhaps get it added the ESPHome core.

However I'd like to get some feedback as to what approach to take here. I'm thinking of a new "meta" cover platform, which controls another cover, e.g:

cover:
  - platform: single_button_control
    control_switch: garage_opener_relay
    control_delay: 1500ms # for when need to stop and change direction
    cover: garage_door # needs to be template or time_based. not sure if other types qualify
    closed_endstop: closed_endstop 
    close_timeout: 16s
    open_endstop: ... # optional
    open_timeout: 16s

Would love some feedback and guidance for architecting this.

@muxa
Copy link
Author

muxa commented Jun 19, 2021

There are actually 2 parts to it:

  1. Controlling the relay from the cover
  2. Controlling the cover from a switch

Controlling relay from the cover is probably a good use case for another cover platform, in addition to template and time_based, let's name it single_button for now:

cover:
  - platform: single_button
    control_output: garage_opener_relay
    control_delay: 1500ms # for when need to stop and change direction
    closed_endstop: closed_endstop 
    close_timeout: 16s
    open_endstop: ... # optional
    open_timeout: 16s

Controlling the cover from a switch is more generic, and can be used as an additional method to control any cover, e.g:

switch:
  - platform: cover_remote
    control_switch: garage_opener_switch
    cover: garage_door
cover:
  - platform: template
    id: garage_door
    ...

@NeilDuToit92
Copy link

This is definitely something that needs to be simplified for the end-users. I agree that a new cover type is needed and I did make a start on it, but never got around to finishing it.

Something I would like to add to your setup an additional binary sensor to allow for a safety beam to be added. I would not want my door (or gate) to be closed remotely if something is in the way.

@NeilDuToit92
Copy link

I would also recommend using config variables that are already in use by other components, such as, but not limited to:

  • close_duration instead of close_timeout
  • open_duration instead of open_timeout
  • output instead of control_output

@muxa
Copy link
Author

muxa commented Jul 8, 2021

@NeilDuToit92 thanks for you input and pointing me to existing config names, better be consistent with existing naming.

Something I would like to add to your setup an additional binary sensor to allow for a safety beam to be added. I would not want my door (or gate) to be closed remotely if something is in the way.

The garage door opener that I have has the obstruction bean built-in. So if garage door is being closed and there's something in the way, it will stop the door. And as per the state diagram above it would time out, so the cover will still remain open.
In my setup I've added a template binary_sensor to indicate if the door is stuck (STATE_OPENING_INTERRUPTED or STATE_CLOSING_INTERRUPTED).

Is this what you had in mind? Or did you mean actually connecting the beam to the ESP?

@NeilDuToit92
Copy link

NeilDuToit92 commented Jul 8, 2021

@NeilDuToit92 thanks for you input and pointing me to existing config names, better be consistent with existing naming.

Something I would like to add to your setup an additional binary sensor to allow for a safety beam to be added. I would not want my door (or gate) to be closed remotely if something is in the way.

The garage door opener that I have has the obstruction bean built-in. So if garage door is being closed and there's something in the way, it will stop the door. And as per the state diagram above it would time out, so the cover will still remain open.
In my setup I've added a template binary_sensor to indicate if the door is stuck (STATE_OPENING_INTERRUPTED or STATE_CLOSING_INTERRUPTED).

Is this what you had in mind? Or did you mean actually connecting the beam to the ESP?

I meant connecting the beam directly to ESP - Where I live almost none of the door motors/controllers has this functionality built in. Our motors only stop if there is physically something stopping the door and there is too much strain on the motor - and even this isn't tuned correctly by a lot of installers.

Not having a beam is fine when you can see the door you are closing when you are in radio range, but the moment you add remote control to it, it becomes a necessity to be able to add a beam in case something is in the way.

Edit: A lot of our sliding gates have a beam built-in, but not the doors.

@muxa
Copy link
Author

muxa commented Jul 10, 2021

Thinking about possible scenarios of obstruction with an additional obstruction sensor:

  1. Door already moving when obstructed: should stop.
  2. Obstructed before pressing the button: the door should not move.
  3. Obstruction remove: do nothing and require a manual interaction to move the door.

Scenario 1 is possible with an automation on the obstruction sensor, e.g:

binary_sensor:
  platform: gpio
  pin: D1
  on_press:
    cover.stop: my_cover

Scenario 2 however will require the proposed single_button cover platform to support it natively, so that no door movement is triggered until obstruction removed.

Here's an updated proposed schema taking into consideration suggested renames and obstruction sensor:

cover:
  - platform: single_button
    output: garage_opener_relay # relay that controls garage door movement
    off_delay: 200ms # how long to keep the relay engaged
    binary_sensor: button # external button for controlling the garage door with
    reverse_delay: 1500ms # for when need to stop and change direction
    closed_endstop: closed_endstop 
    close_duration: 16s # used for timeout
    open_endstop: ... # optional
    open_duration: 16s # used for timeout
    obstruction_sensor: ... # optional

Alternatively can implement obstruction detection in a more generic way within single_button cover, so that obstruction status is controlled from external sensor, e.g:

binary_sensor:
  platform: gpio
  pin: D1
  on_press:
    lambda: id(my_cover).obstructed(true);
  on_release:
    lambda: id(my_cover).obstructed(false);

@nuttytree
Copy link

nuttytree commented Jul 21, 2021

I created my own custom garage door component (not built to be able to use as an external component although I have been considering changing that). One thing to note that would need to be accounted for is how the opener responds to button presses while in motion. The one door I have this currently built for has the following state progression:
opening -> stopped -> closing -> stopped -> opening
However my other garage door has this state progression:
opening -> stopped -> closing -> opening

@muxa
Copy link
Author

muxa commented Aug 17, 2021

I have another use case for a gate which has the following state progression:

opening -> closing -> opening

I.e pressing the button changes direction immediately (no stopping).

So there are 3 possible use cases (including @nuttytree’s), which means the single_button needs to be flexible enough to configure different state transitions.

One way to solve it is to create an abstraction for the state machine and have different implementations of it so that the the right one can be chosen for the single_button in yaml.

I still don’t have a clear idea how to best design this. Will need to think about this some more.

@muxa
Copy link
Author

muxa commented Aug 19, 2021

I'm thinking that first step is to create generic code for a state machine, so that it can be easily configured (in C code). I'm thinking something along these lines:

struct StateTransition
{
    BYTE from_state;
    BYTE trigger;
    BYTE to_state;
};

class StateMachine 
{
public:
    StateMachine(const StateTransition transitions[], int transitionCount, BYTE initialState = 0);

    BYTE current_state();

    bool is_transition_allowed(BYTE trigger);

    BYTE transition(BYTE trigger);
    
private:    
    const StateTransition* _transitions;
    int _transitionCount;
    BYTE _currentState;
};

And then the actual configuration for the state machine can be done like this:

const extern BYTE GATE_STATE_UNKNOWN = 0;
const extern BYTE GATE_STATE_CLOSED = 1;
const extern BYTE GATE_STATE_OPENING = 2;
const extern BYTE GATE_STATE_OPEN = 3;
const extern BYTE GATE_STATE_CLOSING = 5;
const extern BYTE GATE_STATE_WAITING_HOLD = 6;
const extern BYTE GATE_STATE_OPEN_HOLD = 7;

const extern BYTE GATE_TRIGGER_UNKNOWN = 0;
const extern BYTE GATE_TRIGGER_PRESS = 1;
const extern BYTE GATE_TRIGGER_TIMEOUT = 2;

static const StateTransition gate_state_transitions[] =
{
    // from_state                trigger                 to_state
    { GATE_STATE_CLOSED,        GATE_TRIGGER_PRESS,     GATE_STATE_OPENING },
    { GATE_STATE_OPENING,       GATE_TRIGGER_PRESS,     GATE_STATE_CLOSING },
    { GATE_STATE_OPENING,       GATE_TRIGGER_TIMEOUT,   GATE_STATE_OPEN },
    { GATE_STATE_OPEN,          GATE_TRIGGER_PRESS,     GATE_STATE_CLOSING },
    { GATE_STATE_OPEN,          GATE_TRIGGER_TIMEOUT,   GATE_STATE_WAITING_HOLD },
    { GATE_STATE_CLOSING,       GATE_TRIGGER_TIMEOUT,   GATE_STATE_CLOSED },
    { GATE_STATE_CLOSING,       GATE_TRIGGER_PRESS,     GATE_STATE_OPENING },
    { GATE_STATE_WAITING_HOLD,  GATE_TRIGGER_PRESS,     GATE_STATE_OPEN_HOLD },
    { GATE_STATE_WAITING_HOLD,  GATE_TRIGGER_TIMEOUT,   GATE_STATE_OPEN },
    { GATE_STATE_OPEN_HOLD,     GATE_TRIGGER_PRESS,     GATE_STATE_CLOSING },
};

auto stateMachine = new StateMachine(gate_state_transitions, sizeof(gate_state_transitions)/sizeof(StateTransition), GATE_STATE_CLOSED);

@muxa
Copy link
Author

muxa commented Aug 19, 2021

I know such abstractions don't exist, but sketching this here in case it's actually possible:

state_machine:
      name_prefix: GATE_
      states:
        - CLOSED
        - OPENING
        - OPEN
        - CLOSING
        - WAITING_HOLD
        - OPEN_HOLD
      triggers:
        - PRESS
        - TIMEOUT
      transitions:
        - from: CLOSED
          trigger: PRESS
          to: OPENING
        - from: OPENING
          trigger: PRESS
          to: CLOSING
        - from: OPENING
          trigger: TIMEOUT
          to: OPEN
        ...
    on_transition:
      then:
        - lambda: |-
            // x.from_stage, x.trigger, x.to_state
           switch (x.to_state) {
            case GATE_STATE_OPEN:
              id(gate_cover).current_operation = esphome::cover::COVER_OPERATION_IDLE;
              id(gate_cover).position = esphome::cover::COVER_OPEN;
              id(gate_cover).publish_state();
              break;
            case GATE_STATE_OPENING:
              id(gate_cover).current_operation = esphome::cover::COVER_OPERATION_OPENING;
              id(gate_cover).position = esphome::cover::COVER_OPEN;
              id(gate_cover).publish_state();
              break;
          ...
          if (x.to_state == GATE_TRIGGER_TIMEOUT) {
            // TODO: start timeout
          }

And use it by something like this

binary_sensor:
  ...
  on_press:
    then:
      - state_machine.trigger: PRESS

@muxa
Copy link
Author

muxa commented Aug 19, 2021

Could use the new select type to create this in state machine platform, e.g:

select:
  - platform: state_machine
    id: sm
    internal: true
    options:
      - CLOSED
      - OPENING
      - OPEN
      - CLOSING
    triggers:
      - PRESS
      - TIMEOUT
    transitions:
      - from: CLOSED
        trigger: PRESS
        to: OPENING
     - ..
     initial_option: CLOSED

@muxa
Copy link
Author

muxa commented Aug 19, 2021

The timeout trigger can be built-in, so can configure transitions that happen automatically after certain timeout, e.g:

    transitions:
      - from: OPENING
        trigger: TIMEOUT
        to: OPEN
        timeout: 8s

@muxa
Copy link
Author

muxa commented Aug 19, 2021

Then can have a specialised cover platform that supports the state machine:

cover:
  - platform: state_machine
    state_machine_select: sm
    states: # maps state machine states to the cover states
      closed: CLOSED
      opening: OPENING
      open: OPEN
      closing: CLOSING

And to drive the relay from the cover define additional triggers on the state machine:

select:
  - platform: state_machine
    ...
    triggers:
      - PRESS
      - TIMEOUT
      - OPEN
      - CLOSE
      - STOP

And configure cover to fire necessary triggers:

cover:
  - platform: state_machine
    ...
    open_trigger: OPEN
    stop_trigger: STOP
    close_trigger: CLOSE

Or could just use a template cover and use lambda to interact with the state machine.

@muxa
Copy link
Author

muxa commented Aug 19, 2021

When configuring a state machine, should be able to specify optional actions to call on transitions, e.g:

    transitions:
      - from: CLOSED
        trigger: OPEN  # fired when opening the cover from HA
        to: OPENING
        action:
          then:
          - output.turn_on: relay # this will "press" the button to physically control the door/gate 
          - delay: 8s # with this, we don't really need a separate `timeout` attribute on each trigger
          - lambda: 'id(sm).transition("TRIGGER_TIMEOUT");'

Callback for when transitioned to a new state is already in select: on_value.

@muxa
Copy link
Author

muxa commented Aug 23, 2021

I managed to implement a state machine text sensor. I wanted to create a general purpose state machine which can be used to model logic of different single button covers and yet allows using it for other purposes too. Here's a very basic example:

text_sensor:
  - platform: state_machine
    initial_state: CLOSED
    states:
      - CLOSED
      - name: OPENING
        on_enter:
          - delay: 3s
          - state_machine.transition: TIMEOUT
      - name: CLOSING
        on_enter:
          - delay: 3s
          - state_machine.transition: TIMEOUT
      - CLOSING
    inputs:
      - name: PRESS
        transitions:
          - CLOSED -> OPENING
          - OPENING -> CLOSING
          - CLOSING -> OPENING
          - OPEN -> CLOSING
      - name: TIMEOUT
        transitions:
          - OPENING -> OPEN
          - CLOSING -> CLOSED
        action:
          - logger.log: "Timeout reached"

The states block allows you defining available states. Each state can be a simple string, or can be expanded to have an on_enter trigger. Handy for timeouts.

The inputs block allows defining the state machine inputs (also sometimes referred to as triggers) with a list if allowed transitions. Each input can also have an optional action to trigger when this input is received.

The state_machine.transition allows to provide input to the state machine from automation (for TIMEOUT in the above example).

Also sometimes you want to have a conditional action in a state. For that I've added state_machine.transition condition. E.g.:

      - name: OPEN
        on_enter:
          - if:
              condition:
                state_machine.transition:
                  from: OPENING
              then:
                - logger.log: "Will be able to HOLD in 6s"
                - delay: 6s
                - state_machine.transition: TIMEOUT
      - name: WAITING_HOLD
        on_enter:
          - logger.log: "Waiting for HOLD"
          - delay: 4s
          - state_machine.transition: TIMEOUT
      - name: OPEN_HOLD
    inputs:
      - name: PRESS
        transitions:
          ...
          - WAITING_HOLD -> OPEN_HOLD
          - OPEN_HOLD -> CLOSING
      - name: TIMEOUT
        transitions:
          ...
          - OPEN -> WAITING_HOLD
          - WAITING_HOLD -> OPEN

In the next few days I'll get the code cleanup up and ready for use as an external component.

@muxa
Copy link
Author

muxa commented Aug 24, 2021

Uploaded a working version: https://github.com/muxa/esphome-state-machine/tree/dev

Usage:

  - source:
      type: git
      url: https://github.com/muxa/esphome-state-machine
      ref: dev

@muxa
Copy link
Author

muxa commented Aug 26, 2021

Closing this issue as now with the above mentioned state machine component I can model any logic of gates and door controllers with relative ease.

@muxa muxa closed this as completed Aug 26, 2021
@Tuckie
Copy link

Tuckie commented Sep 15, 2021

@muxa do you have an example of your component with the single button cover control?

@muxa
Copy link
Author

muxa commented Sep 18, 2021

@Tuckie yes, here are some examples:

@Tuckie
Copy link

Tuckie commented Sep 18, 2021

Thank you so much! I've ordered some M5stack relay modules that I plan on using.

@Douganatornz
Copy link

@Tuckie yes, here are some examples:

* Garage door with single button and contact sensor to detect closed: https://gist.github.com/muxa/68a6e8f8cdbb734c9e0d06a9b87d0147

* Gate control with a single button: https://gist.github.com/muxa/f6aeda90eec739e79a645146364d563d

Hi @muxa, thanks for the single button and contact sensor code . .. . it's blowing my mind through ahahaa. I would like some help with a very similar single button sketch if possible.
Using a D1 + Relay to toggle the garage door (so I guess single button) Pin D1
Contact sensor on Pin D8
No lights
There seems to be a lot of code in your sketch that I would not need?
Would appreciate some help if you have time.

@muxa
Copy link
Author

muxa commented Oct 20, 2021

@Douganatornz can you describe your garage door control setup in full? I.e. do you have a secondary control? (e.g. original garage remote), do you have a contact sensor to detect when the garage door is fully closed or open?, etc.
In order to build a reliable state machine all inputs that can change the state of the garage door need to be taken into account.

@Douganatornz
Copy link

@Douganatornz can you describe your garage door control setup in full? I.e. do you have a secondary control? (e.g. original garage remote), do you have a contact sensor to detect when the garage door is fully closed or open?, etc. In order to build a reliable state machine all inputs that can change the state of the garage door need to be taken into account.

@muxa
I have a very simple/standard setup.
Single garage door/motor
RF Remote
ESP8266 with single contact reply wired into motor input
Contact sensor wired to ESP8266 to show closed state

What I would like to do using this new ESPHome code is to show states CLOSED, OPENING, OPEN, CLOSING.
I guess this could be achieved by timings?
Also I would like a small delay on the contact sensor state change just because when the door is opening the state changes many times in the first second of the door opening.

Thanks

@muxa
Copy link
Author

muxa commented Oct 24, 2021

@Douganatornz I'd like to transfer this discussion to https://github.com/muxa/esphome-state-machine/discussions
Can you please copy & paste your last message into a new discussion on https://github.com/muxa/esphome-state-machine/discussions/new where I will respond with more details? That way this use case will be together with other ones and will hopefully make it easier for other people to find relevant information in one place.

@github-actions github-actions bot locked and limited conversation to collaborators Feb 22, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants