From 3e3f010d15c0f4bba37467a1ed797e430f52d733 Mon Sep 17 00:00:00 2001 From: Mikhail Diatchenko Date: Mon, 30 May 2022 18:49:11 +1200 Subject: [PATCH 1/3] Reworked input and transition actions (breaking) --- README.md | 21 +++- components/state_machine/__init__.py | 107 ++++++++++++++---- components/state_machine/automation.h | 61 +++++++--- components/state_machine/state_machine.cpp | 3 +- components/state_machine/state_machine.h | 6 +- .../text_sensor/state_machine_text_sensor.cpp | 2 +- dimmable-light-example.yaml | 6 +- test.yaml | 75 ++++++++++++ 8 files changed, 234 insertions(+), 47 deletions(-) create mode 100644 test.yaml diff --git a/README.md b/README.md index e901794..d41ae0e 100644 --- a/README.md +++ b/README.md @@ -59,8 +59,8 @@ binary_sensor: * **states** (**Required**, list): The list of states that the state machine has. * **name** (**Required**, string): The name of the state. Must not repeat. - * **on_enter** (*Optional*, [Automation](https://esphome.io/guides/automations.html#automation)): An automation to perform when entering this state. - * **on_leave** (*Optional*, [Automation](https://esphome.io/guides/automations.html#automation)): An automation to perform when leaving this state. It called before `on_enter` of the next state. + * **on_enter** (*Optional*, [Automation](https://esphome.io/guides/automations.html#automation)): An automation to perform when entering this state. Called after `on_transition` automation and before `after_transition`. + * **on_leave** (*Optional*, [Automation](https://esphome.io/guides/automations.html#automation)): An automation to perform when leaving this state. Called after `before_transition` automation and before `on_transition` automation. * **on_set** (*Optional*, [Automation](https://esphome.io/guides/automations.html#automation)): An automation to perform when setting this state using `set` action or via `initial_state`. Will not trigger if new state is the same as current state. * **inputs** (**Required**, list): The list of inputs that the state machine supports with allowed state transitions. @@ -69,8 +69,10 @@ binary_sensor: * **transitions** (**Required**, list): The list of allowed transitions. Short form is `FROM_STATE -> TO_STATE`, or advanced configuration: * **from** (**Required**, string): Source state that this input is allowed on. * **to** (**Required**, string): Target state that this input transitions to. - * **action** (*Optional*, [Automation](https://esphome.io/guides/automations.html#automation)): An automation to perform when transition is performed. This action is performed before state's `on_leave` action is called. - * **action** (*Optional*, [Automation](https://esphome.io/guides/automations.html#automation)): An automation to perform when transition is done by this input. This action is performed after transition-specific action and before state's `on_leave` action is called. + * **before_transition** (*Optional*, [Automation](https://esphome.io/guides/automations.html#automation)): An automation to perform before transition. Called after `on_input` automation and before `on_transition` automation. + * **on_transition** (*Optional*, [Automation](https://esphome.io/guides/automations.html#automation)): An automation to perform on transition. Called after `on_leave` automation and before `on_enter` automation. + * **after_transition** (*Optional*, [Automation](https://esphome.io/guides/automations.html#automation)): An automation to perform after transition. Called after `on_enter` automation. + * **on_input** (*Optional*, [Automation](https://esphome.io/guides/automations.html#automation)): An automation to perform when this input is triggred. This automation is performed first, then `before_transition ` will be called. This automation will not be called if transition is invalid. * **diagram** (*Optional*, boolean): If true, then a diagram of the state machine will be output to the console during validation/compilation of YAML. See **Diagrams** section below for more details. Defaults to `false`. @@ -78,6 +80,17 @@ binary_sensor: > > Any running state machine automations (state, input and transition) will be stopped before running next automations. This is useful when there's a delayed transition in one of the automation and it needs to be cancelled because a new input was provided which results in a different transition. +## Order of Triggers + +When State Machine receives input (via `transition` action) it will call the automation specified in the input, transition, the "from" state and the "to" state in the following order: + +1. `on_input` +2. `before_transition` +3. "From" state `on_leave` +4. `on_transition` +5. "To" state `on_enter` +6. `after_transition` + ## `state_machine.transition` Action You can provide input to the state machine from elsewhere in your WAML file with the `state_machine.transition` action. diff --git a/components/state_machine/__init__.py b/components/state_machine/__init__.py index c8d7f26..7f1db2b 100644 --- a/components/state_machine/__init__.py +++ b/components/state_machine/__init__.py @@ -39,12 +39,20 @@ "StateMachineOnLeaveTrigger", automation.Trigger.template() ) -StateMachineInputActionTrigger = state_machine_ns.class_( - "StateMachineInputActionTrigger", automation.Trigger.template() +StateMachineOnInputTrigger = state_machine_ns.class_( + "StateMachineOnInputTrigger", automation.Trigger.template() ) -StateMachineTransitionActionTrigger = state_machine_ns.class_( - "StateMachineTransitionActionTrigger", automation.Trigger.template() +StateMachineBeforeTransitionTrigger = state_machine_ns.class_( + "StateMachineBeforeTransitionTrigger", automation.Trigger.template() +) + +StateMachineOnTransitionTrigger = state_machine_ns.class_( + "StateMachineOnTransitionTrigger", automation.Trigger.template() +) + +StateMachineAfterTransitionTrigger = state_machine_ns.class_( + "StateMachineAfterTransitionTrigger", automation.Trigger.template() ) StateMachineSetAction = state_machine_ns.class_("StateMachineSetAction", automation.Action) @@ -64,6 +72,12 @@ CONF_STATE_ON_ENTER_KEY = 'on_enter' CONF_STATE_ON_LEAVE_KEY = 'on_leave' CONF_INPUT_TRANSITIONS_KEY = 'transitions' +CONF_BEFORE_TRANSITION_KEY = 'before_transition' +CONF_ON_TRANSITION_KEY = 'on_transition' +CONF_AFTER_TRANSITION_KEY = 'after_transition' +CONF_ON_INPUT_KEY = 'on_input' + +# deprecated CONF_INPUT_TRANSITIONS_ACTION_KEY = 'action' CONF_INPUT_ACTION_KEY = 'action' @@ -79,11 +93,22 @@ def validate_transition(value): { cv.Required(CONF_FROM): cv.string_strict, cv.Required(CONF_TO): cv.string_strict, - cv.Optional(CONF_INPUT_TRANSITIONS_ACTION_KEY): automation.validate_automation( + cv.Optional(CONF_BEFORE_TRANSITION_KEY): automation.validate_automation( { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateMachineTransitionActionTrigger), + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateMachineBeforeTransitionTrigger), } - ) + ), + cv.Optional(CONF_ON_TRANSITION_KEY): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateMachineOnTransitionTrigger), + } + ), + cv.Optional(CONF_AFTER_TRANSITION_KEY): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateMachineAfterTransitionTrigger), + } + ), + cv.Optional(CONF_INPUT_TRANSITIONS_ACTION_KEY): cv.invalid("`action` is deprecated. Please use one of `before_transition`, `on_transition` or `after_transition` instead"), } )(value) value = cv.string(value) @@ -195,11 +220,12 @@ def unique_names(items): cv.ensure_list(cv.maybe_simple_value( { cv.Required(CONF_NAME): cv.string_strict, - cv.Optional(CONF_INPUT_ACTION_KEY): automation.validate_automation( + cv.Optional(CONF_ON_INPUT_KEY): automation.validate_automation( { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateMachineInputActionTrigger), + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateMachineOnInputTrigger), } ), + cv.Optional(CONF_INPUT_ACTION_KEY): cv.invalid("`action` is deprecated. Please use `on_input` instead"), cv.Optional(CONF_INPUT_TRANSITIONS_KEY): cv.All( cv.ensure_list(validate_transition), cv.Length(min=1) ), @@ -271,12 +297,22 @@ async def to_code(config): ) await automation.build_automation(trigger, [], action) - # 1. setup transition/input automations (they should run first) + # setup transition/input automations (they should run first) for input in config[CONF_INPUTS_KEY]: + + # 1. on_input automations + if CONF_ON_INPUT_KEY in input: + for action in input.get(CONF_ON_INPUT_KEY, []): + trigger = cg.new_Pvariable( + action[CONF_TRIGGER_ID], var, input[CONF_NAME] + ) + await automation.build_automation(trigger, [], action) + + # 2. before_transition automations if CONF_INPUT_TRANSITIONS_KEY in input: for transition in input[CONF_INPUT_TRANSITIONS_KEY]: - if CONF_INPUT_TRANSITIONS_ACTION_KEY in transition: - for action in transition.get(CONF_INPUT_TRANSITIONS_ACTION_KEY, []): + if CONF_BEFORE_TRANSITION_KEY in transition: + for action in transition.get(CONF_BEFORE_TRANSITION_KEY, []): trigger = cg.new_Pvariable( action[CONF_TRIGGER_ID], var, @@ -289,14 +325,7 @@ async def to_code(config): ) await automation.build_automation(trigger, [], action) - if CONF_INPUT_ACTION_KEY in input: - for action in input.get(CONF_INPUT_ACTION_KEY, []): - trigger = cg.new_Pvariable( - action[CONF_TRIGGER_ID], var, input[CONF_NAME] - ) - await automation.build_automation(trigger, [], action) - - # 2. setup on_leave automations (to ensure they are executed before on_enter) + # 3. on_leave automations for state in config[CONF_STATES_KEY]: if CONF_STATE_ON_LEAVE_KEY in state: @@ -306,7 +335,25 @@ async def to_code(config): ) await automation.build_automation(trigger, [], action) - # 3. setup on_enter automations after on_leave + # 4. on_transition automations + for input in config[CONF_INPUTS_KEY]: + if CONF_INPUT_TRANSITIONS_KEY in input: + for transition in input[CONF_INPUT_TRANSITIONS_KEY]: + if CONF_ON_TRANSITION_KEY in transition: + for action in transition.get(CONF_ON_TRANSITION_KEY, []): + trigger = cg.new_Pvariable( + action[CONF_TRIGGER_ID], + var, + cg.StructInitializer( + StateTransition, + ("from_state", transition[CONF_FROM]), + ("input", input[CONF_NAME]), + ("to_state", transition[CONF_TO]), + ) + ) + await automation.build_automation(trigger, [], action) + + # 5. on_enter automations for state in config[CONF_STATES_KEY]: if CONF_STATE_ON_ENTER_KEY in state: @@ -316,6 +363,24 @@ async def to_code(config): ) await automation.build_automation(trigger, [], action) + # 6. after_transition automations + for input in config[CONF_INPUTS_KEY]: + if CONF_INPUT_TRANSITIONS_KEY in input: + for transition in input[CONF_INPUT_TRANSITIONS_KEY]: + if CONF_AFTER_TRANSITION_KEY in transition: + for action in transition.get(CONF_AFTER_TRANSITION_KEY, []): + trigger = cg.new_Pvariable( + action[CONF_TRIGGER_ID], + var, + cg.StructInitializer( + StateTransition, + ("from_state", transition[CONF_FROM]), + ("input", input[CONF_NAME]), + ("to_state", transition[CONF_TO]), + ) + ) + await automation.build_automation(trigger, [], action) + await cg.register_component(var, config) cg.add(var.dump_config()) diff --git a/components/state_machine/automation.h b/components/state_machine/automation.h index acc541b..8435f39 100644 --- a/components/state_machine/automation.h +++ b/components/state_machine/automation.h @@ -26,16 +26,32 @@ namespace esphome } }; - class StateMachineOnEnterTrigger : public Trigger<> + class StateMachineOnInputTrigger : public Trigger<> { public: - StateMachineOnEnterTrigger(StateMachineComponent *state_machine, std::string state) + StateMachineOnInputTrigger(StateMachineComponent *state_machine, std::string input) { - state_machine->add_on_transition_callback( - [this, state](StateTransition transition) + state_machine->add_before_transition_callback( + [this, input](StateTransition transition) { this->stop_action(); // stop any previous running actions - if (transition.to_state == state) + if (transition.input == input) + { + this->trigger(); + } + }); + } + }; + class StateMachineBeforeTransitionTrigger : public Trigger<> + { + public: + StateMachineBeforeTransitionTrigger(StateMachineComponent *state_machine, StateTransition for_transition) + { + state_machine->add_before_transition_callback( + [this, for_transition](StateTransition transition) + { + this->stop_action(); // stop any previous running actions + if (transition.from_state == for_transition.from_state && transition.input == for_transition.input && transition.to_state == for_transition.to_state) { this->trigger(); } @@ -48,7 +64,7 @@ namespace esphome public: StateMachineOnLeaveTrigger(StateMachineComponent *state_machine, std::string state) { - state_machine->add_on_transition_callback( + state_machine->add_before_transition_callback( [this, state](StateTransition transition) { this->stop_action(); // stop any previous running actions @@ -59,13 +75,12 @@ namespace esphome }); } }; - - class StateMachineTransitionActionTrigger : public Trigger<> + class StateMachineOnTransitionTrigger : public Trigger<> { public: - StateMachineTransitionActionTrigger(StateMachineComponent *state_machine, StateTransition for_transition) + StateMachineOnTransitionTrigger(StateMachineComponent *state_machine, StateTransition for_transition) { - state_machine->add_on_transition_callback( + state_machine->add_before_transition_callback( [this, for_transition](StateTransition transition) { this->stop_action(); // stop any previous running actions @@ -77,16 +92,32 @@ namespace esphome } }; - class StateMachineInputActionTrigger : public Trigger<> + class StateMachineOnEnterTrigger : public Trigger<> { public: - StateMachineInputActionTrigger(StateMachineComponent *state_machine, std::string input) + StateMachineOnEnterTrigger(StateMachineComponent *state_machine, std::string state) { - state_machine->add_on_transition_callback( - [this, input](StateTransition transition) + state_machine->add_after_transition_callback( + [this, state](StateTransition transition) { this->stop_action(); // stop any previous running actions - if (transition.input == input) + if (transition.to_state == state) + { + this->trigger(); + } + }); + } + }; + class StateMachineAfterTransitionTrigger : public Trigger<> + { + public: + StateMachineAfterTransitionTrigger(StateMachineComponent *state_machine, StateTransition for_transition) + { + state_machine->add_after_transition_callback( + [this, for_transition](StateTransition transition) + { + this->stop_action(); // stop any previous running actions + if (transition.from_state == for_transition.from_state && transition.input == for_transition.input && transition.to_state == for_transition.to_state) { this->trigger(); } diff --git a/components/state_machine/state_machine.cpp b/components/state_machine/state_machine.cpp index feaad1f..595d468 100644 --- a/components/state_machine/state_machine.cpp +++ b/components/state_machine/state_machine.cpp @@ -96,10 +96,11 @@ namespace esphome optional transition = this->get_transition(input); if (transition) { + this->before_transition_callback_.call(transition.value()); ESP_LOGD(TAG, "%s: transitioned from %s to %s", input.c_str(), transition.value().from_state.c_str(), transition.value().to_state.c_str()); this->last_transition_ = transition; this->current_state_ = transition.value().to_state; - this->transition_callback_.call(transition.value()); + this->after_transition_callback_.call(transition.value()); } else { diff --git a/components/state_machine/state_machine.h b/components/state_machine/state_machine.h index 0eea55d..82a8ae3 100644 --- a/components/state_machine/state_machine.h +++ b/components/state_machine/state_machine.h @@ -36,7 +36,8 @@ namespace esphome optional transition(std::string input); void add_on_set_callback(std::function &&callback) { this->set_callback_.add(std::move(callback)); } - void add_on_transition_callback(std::function &&callback) { this->transition_callback_.add(std::move(callback)); } + void add_before_transition_callback(std::function &&callback) { this->before_transition_callback_.add(std::move(callback)); } + void add_after_transition_callback(std::function &&callback) { this->after_transition_callback_.add(std::move(callback)); } protected: std::string name_; @@ -50,7 +51,8 @@ namespace esphome optional get_transition(std::string input); CallbackManager set_callback_{}; - CallbackManager transition_callback_{}; + CallbackManager before_transition_callback_{}; + CallbackManager after_transition_callback_{}; }; } // namespace state_machine diff --git a/components/state_machine/text_sensor/state_machine_text_sensor.cpp b/components/state_machine/text_sensor/state_machine_text_sensor.cpp index 3798bd8..e5bb51a 100644 --- a/components/state_machine/text_sensor/state_machine_text_sensor.cpp +++ b/components/state_machine/text_sensor/state_machine_text_sensor.cpp @@ -10,7 +10,7 @@ namespace esphome void StateMachineTextSensor::set_state_machine(StateMachineComponent *state_machine) { this->state_machine_ = state_machine; - this->state_machine_->add_on_transition_callback( + this->state_machine_->add_after_transition_callback( [this](StateTransition transition) { this->update(); diff --git a/dimmable-light-example.yaml b/dimmable-light-example.yaml index ee69c01..2e18323 100644 --- a/dimmable-light-example.yaml +++ b/dimmable-light-example.yaml @@ -27,7 +27,7 @@ state_machine: - OFF -> ON - from: EDITING to: EDITING - action: # cycle through brightness levels + on_transition: # cycle through brightness levels - lambda: |- auto call = id(light1).turn_on(); auto brightness = id(light1).current_values.get_brightness(); @@ -41,7 +41,7 @@ state_machine: transitions: - from: "ON" to: EDITING - action: # single flash to indicate beginning of editing + on_transition: # single flash to indicate beginning of editing - light.turn_on: id: light1 effect: Strobe @@ -52,7 +52,7 @@ state_machine: transition_length: 100ms - from: EDITING to: "ON" - action: # tripple flash to indicate end of editing + on_transition: # tripple flash to indicate end of editing - light.turn_on: id: light1 effect: Strobe diff --git a/test.yaml b/test.yaml new file mode 100644 index 0000000..25f874e --- /dev/null +++ b/test.yaml @@ -0,0 +1,75 @@ +esphome: + name: test + platform: ESP8266 + board: d1_mini + +logger: + level: DEBUG + +external_components: + - source: components + +text_sensor: + - platform: state_machine + name: On/Off Toggle State Machine + +state_machine: + - name: On/Off Toggle State Machine + states: + - name: "OFF" + on_enter: + - output.turn_off: led1 + - logger.log: "5. [OFF] on_enter. [ON->OFF] after_transition should be called next" + on_leave: + - logger.log: "3. [OFF] on_leave. [OFF->ON] on_transition should be called next" + - name: "ON" + on_enter: + - output.turn_on: led1 + - logger.log: "5. [ON] on_enter. [OFF->ON] after_transition should be called next" + on_leave: + - logger.log: "3. [ON] on_leave. [ON->OFF] on_transition should be called next" + inputs: + - name: TOGGLE + transitions: + - from: "ON" + to: "OFF" + # action: + # - logger.log: "Turning off" + before_transition: + - logger.log: "2. [ON->OFF] before_transition. [ON] on_leave should be called next" + on_transition: + - logger.log: "4. [ON->OFF] on_transition. [OFF] on_enter should be called next" + after_transition: + - logger.log: "6. [ON->OFF] after_transition. done" + - from: "OFF" + to: "ON" + before_transition: + - logger.log: "2. [OFF->ON] before_transition. [OFF] on_leave should be called next" + on_transition: + - logger.log: "4. [OFF->ON] on_transition. [ON] on_enter should be called next" + after_transition: + - logger.log: "6. [OFF->ON] after_transition. done" + # action: + # - logger.log: "Toggling" + on_input: + - logger.log: "1. on_input. before_transition should be called next" + diagram: mermaid + +binary_sensor: + - platform: gpio + pin: + number: D6 + mode: INPUT_PULLUP + inverted: True + name: "Button" + filters: + - delayed_on: 100ms + on_press: + - state_machine.transition: TOGGLE + +output: + - platform: gpio + pin: + number: D4 + inverted: True + id: led1 \ No newline at end of file From 8c3fed855497920f3793c4c3d8c4ff4f0002d61d Mon Sep 17 00:00:00 2001 From: Mikhail Diatchenko Date: Mon, 6 Jun 2022 17:38:35 +1200 Subject: [PATCH 2/3] Added another test to CI --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 87e0766..b38635c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,6 +30,7 @@ jobs: pip install esphome - name: ESPHome Compile examples run: | + esphome compile test.yaml esphome compile toggle-example.yaml esphome compile dimmable-light-example.yaml esphome compile dual-switch-cover-example.yaml From e42e381d07f7d313cf8fe9b4b20f9875fb5ae485 Mon Sep 17 00:00:00 2001 From: Mikhail Diatchenko Date: Mon, 6 Jun 2022 18:19:30 +1200 Subject: [PATCH 3/3] Added test.yaml to readme --- README.md | 2 ++ test.yaml | 4 ---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d41ae0e..33534a1 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,8 @@ When State Machine receives input (via `transition` action) it will call the aut 5. "To" state `on_enter` 6. `after_transition` +See [test.yaml](test.yaml) for a demonstration of running order of these automations within a context of simple ON-OFF state machine. + ## `state_machine.transition` Action You can provide input to the state machine from elsewhere in your WAML file with the `state_machine.transition` action. diff --git a/test.yaml b/test.yaml index 25f874e..7d2a7d6 100644 --- a/test.yaml +++ b/test.yaml @@ -33,8 +33,6 @@ state_machine: transitions: - from: "ON" to: "OFF" - # action: - # - logger.log: "Turning off" before_transition: - logger.log: "2. [ON->OFF] before_transition. [ON] on_leave should be called next" on_transition: @@ -49,8 +47,6 @@ state_machine: - logger.log: "4. [OFF->ON] on_transition. [ON] on_enter should be called next" after_transition: - logger.log: "6. [OFF->ON] after_transition. done" - # action: - # - logger.log: "Toggling" on_input: - logger.log: "1. on_input. before_transition should be called next" diagram: mermaid