Skip to content

Commit

Permalink
Merge pull request #27 from muxa/dev
Browse files Browse the repository at this point in the history
Reworked input and transition actions
  • Loading branch information
muxa authored Jun 9, 2022
2 parents 59180e5 + e42e381 commit 8e1c80d
Show file tree
Hide file tree
Showing 9 changed files with 233 additions and 47 deletions.
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
23 changes: 19 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -69,15 +69,30 @@ 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`.

> ### Note:
>
> 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`

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.
Expand Down
107 changes: 86 additions & 21 deletions components/state_machine/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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'

Expand All @@ -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)
Expand Down Expand Up @@ -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)
),
Expand Down Expand Up @@ -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,
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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())
Expand Down
61 changes: 46 additions & 15 deletions components/state_machine/automation.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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();
}
Expand Down
3 changes: 2 additions & 1 deletion components/state_machine/state_machine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,11 @@ namespace esphome
optional<StateTransition> 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
{
Expand Down
6 changes: 4 additions & 2 deletions components/state_machine/state_machine.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ namespace esphome
optional<StateTransition> transition(std::string input);

void add_on_set_callback(std::function<void(std::string)> &&callback) { this->set_callback_.add(std::move(callback)); }
void add_on_transition_callback(std::function<void(StateTransition)> &&callback) { this->transition_callback_.add(std::move(callback)); }
void add_before_transition_callback(std::function<void(StateTransition)> &&callback) { this->before_transition_callback_.add(std::move(callback)); }
void add_after_transition_callback(std::function<void(StateTransition)> &&callback) { this->after_transition_callback_.add(std::move(callback)); }

protected:
std::string name_;
Expand All @@ -50,7 +51,8 @@ namespace esphome
optional<StateTransition> get_transition(std::string input);

CallbackManager<void(std::string)> set_callback_{};
CallbackManager<void(StateTransition)> transition_callback_{};
CallbackManager<void(StateTransition)> before_transition_callback_{};
CallbackManager<void(StateTransition)> after_transition_callback_{};
};

} // namespace state_machine
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
6 changes: 3 additions & 3 deletions dimmable-light-example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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
Expand All @@ -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
Expand Down
Loading

0 comments on commit 8e1c80d

Please sign in to comment.