-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Release 1.0.0
- Loading branch information
Showing
13 changed files
with
985 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -30,3 +30,8 @@ | |
*.exe | ||
*.out | ||
*.app | ||
__pycache__ | ||
.DS_Store | ||
toggle_example | ||
.esphome | ||
dimmable_light_example |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,173 @@ | ||
# esphome-state-machine | ||
State Machine implemented using text_sensor | ||
# ESPHome State Machine | ||
A flexible [Finite-State Machine](https://en.wikipedia.org/wiki/Finite-state_machine) for [ESPHome](https://esphome.io/) implemented on top of a [text_sensor](https://esphome.io/components/text_sensor/index.html). It lets you model complex behaviours with limited inputs, such as: | ||
|
||
* Controlling dimmable `light` with a single button. | ||
* Controlling a garage door `cover` with a single button. | ||
* Controlling a `display` with a button (e.g. flip through pages on click, and go into editing mode on hold). | ||
* And more... | ||
|
||
## Installing | ||
|
||
```yaml | ||
external_components: | ||
- source: | ||
type: git | ||
url: https://github.com/muxa/esphome-state-machine | ||
``` | ||
## Configuration | ||
The basic state machine configuration involves providing: | ||
* A list of `states` | ||
* A list of `inputs` | ||
* A list of allowed `transitions` for each input. | ||
|
||
Example for a simple on/off toggle state machine: | ||
|
||
![Toggle State Machine Diagram](images/state-machine-toggle.svg) | ||
|
||
```yaml | ||
text_sensor: | ||
- platform: state_machine | ||
name: On/Off Toggle State Machine | ||
states: | ||
- "OFF" | ||
- "ON" | ||
inputs: | ||
- name: TOGGLE | ||
transitions: | ||
- ON -> OFF | ||
- OFF -> ON | ||
``` | ||
|
||
And to transition between states it you'll need to trigger the machine by providing input, e.g: | ||
|
||
```yaml | ||
binary_sensor: | ||
- platform: gpio | ||
pin: D6 | ||
name: "Button" | ||
filters: | ||
- delayed_on: 100ms | ||
on_press: | ||
- state_machine.transition: TOGGLE | ||
``` | ||
|
||
## Configuration variables: | ||
|
||
* **initial_state** (**Optional**, string): The intial state of the state machine. Defaults to first defined state. | ||
* **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. | ||
|
||
* **inputs** (**Required**, list): The list of inputs that the state machine supports with allowed state transitions. | ||
|
||
* **name** (**Required**, string): The name of the input. Must not repeat. | ||
* **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. | ||
* **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. | ||
|
||
> ### 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. | ||
|
||
## `state_machine.transition` Action | ||
|
||
You can provide input to the state machine from elsewhere in your WAML file with the `state_machine.transition` action. | ||
```yaml | ||
# in some trigger | ||
on_...: | ||
# Basic: | ||
- state_machine.transition: TOGGLE | ||
# Advanced (if you have multiple state machines in one YAML) | ||
- state_machine.transition: | ||
id: sm1 | ||
input: TOGGLE | ||
``` | ||
|
||
Configuration options: | ||
|
||
* **id** (*Optional*, [ID](https://esphome.io/guides/configuration-types.html#config-id)): The ID of the state machine. | ||
* **input** (**Required**, string): The input to provide in order to transition to the next state. | ||
|
||
## `state_machine.transition` Condition | ||
|
||
This condition lets you check what transition last occurred. | ||
|
||
```yaml | ||
# in some trigger | ||
on_...: | ||
# Basic | ||
if: | ||
condition: | ||
state_machine.transition: | ||
trigger: TOGGLE | ||
then: | ||
- logger.log: Toggled | ||
# Advanced | ||
if: | ||
condition: | ||
state_machine.transition: | ||
id: sm1 | ||
from: "OFF" | ||
trigger: TOGGLE | ||
to: "ON" | ||
then: | ||
- logger.log: Turned on by toggle | ||
``` | ||
|
||
## Diagrams | ||
|
||
When compiling or validating your YAML a state machine diagram will be generated using [DOT notation](https://en.wikipedia.org/wiki/DOT_(graph_description_language)), with a link to view the diagram, e.g: | ||
|
||
``` | ||
State Machine Diagram (for On/Off Toggle State Machine): | ||
https://quickchart.io/graphviz?graph=digraph%20%22On/Off%20Toggle%20State%20Machine%22%20%7B%0A%20%20node%20%5Bshape%3Dellipse%5D%3B%0A%20%20ON%20-%3E%20OFF%20%5Blabel%3DTOGGLE%5D%3B%0A%20%20OFF%20-%3E%20ON%20%5Blabel%3DTOGGLE%5D%3B%0A%7D | ||
digraph "On/Off Toggle State Machine" { | ||
node [shape=ellipse]; | ||
ON -> OFF [label=TOGGLE]; | ||
OFF -> ON [label=TOGGLE]; | ||
} | ||
``` | ||
|
||
To get just the url use this command: | ||
|
||
```bash | ||
esphome config <config.yaml> 2>/dev/null | grep quickchart.io | ||
``` | ||
|
||
To open the diagram in Chrome use this command: | ||
|
||
```bash | ||
esphome config <config.yaml> 2>/dev/null | grep quickchart.io | xargs open -n -a "Google Chrome" --args "-0" | ||
``` | ||
|
||
|
||
## All Examples | ||
|
||
### Simple Toggle | ||
|
||
![Simple Toggle State Machine Diagram](images/state-machine-toggle.svg) | ||
|
||
This example illustrates toggling an LED using a button. | ||
|
||
See [toggle_example.yaml](toggle_example.yaml). | ||
|
||
### Button Controlled Dimmable Light | ||
|
||
![Button Controlled Dimmable Light State Machine Diagram](images/state-machine-brightness.svg) | ||
|
||
This example models a single button control for a dimmable light with the following functionality: | ||
* CLICK to toggle ON of OFF | ||
* HOLD to go into EDITING mode to adjust brightness with a CLICK. | ||
|
||
See [dimmable_light_example.yaml](dimmable_light_example.yaml). | ||
|
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
#pragma once | ||
|
||
#include "esphome/core/component.h" | ||
#include "esphome/core/automation.h" | ||
#include "state_machine_text_sensor.h" | ||
|
||
namespace esphome | ||
{ | ||
|
||
class StateMachineOnEnterTrigger : public Trigger<> | ||
{ | ||
public: | ||
StateMachineOnEnterTrigger(StateMachineTextSensor *text_sensor, std::string state) | ||
{ | ||
text_sensor->add_on_transition_callback( | ||
[this, state](StateTransition transition) | ||
{ | ||
this->stop_action(); // stop any previous running actions | ||
if (transition.to_state == state) | ||
{ | ||
this->trigger(); | ||
} | ||
}); | ||
} | ||
}; | ||
|
||
class StateMachineOnLeaveTrigger : public Trigger<> | ||
{ | ||
public: | ||
StateMachineOnLeaveTrigger(StateMachineTextSensor *text_sensor, std::string state) | ||
{ | ||
text_sensor->add_on_transition_callback( | ||
[this, state](StateTransition transition) | ||
{ | ||
this->stop_action(); // stop any previous running actions | ||
if (transition.from_state == state) | ||
{ | ||
this->trigger(); | ||
} | ||
}); | ||
} | ||
}; | ||
|
||
class StateMachineTransitionActionTrigger : public Trigger<> | ||
{ | ||
public:StateMachineTransitionActionTrigger(StateMachineTextSensor *text_sensor, StateTransition for_transition) | ||
{ | ||
text_sensor->add_on_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(); | ||
} | ||
}); | ||
} | ||
}; | ||
|
||
class StateMachineInputActionTrigger : public Trigger<> | ||
{ | ||
public: | ||
StateMachineInputActionTrigger(StateMachineTextSensor *text_sensor, std::string input) | ||
{ | ||
text_sensor->add_on_transition_callback( | ||
[this, input](StateTransition transition) | ||
{ | ||
this->stop_action(); // stop any previous running actions | ||
if (transition.input == input) | ||
{ | ||
this->trigger(); | ||
} | ||
}); | ||
} | ||
}; | ||
|
||
template <typename... Ts> | ||
class StateMachineTransitionAction : public Action<Ts...>, public Parented<StateMachineTextSensor> | ||
{ | ||
public: | ||
StateMachineTransitionAction(std::string input) | ||
{ | ||
this->input_ = input; | ||
} | ||
|
||
void play(Ts... x) override { this->parent_->transition(this->input_); } | ||
|
||
protected: | ||
std::string input_; | ||
}; | ||
|
||
template <typename... Ts> | ||
class StateMachineTransitionCondition : public Condition<Ts...> | ||
{ | ||
public: | ||
explicit StateMachineTransitionCondition(StateMachineTextSensor *parent) : parent_(parent) {} | ||
|
||
TEMPLATABLE_VALUE(std::string, from_state) | ||
TEMPLATABLE_VALUE(std::string, input) | ||
TEMPLATABLE_VALUE(std::string, to_state) | ||
|
||
bool check(Ts... x) override | ||
{ | ||
if (!this->parent_->last_transition.has_value()) | ||
return false; | ||
StateTransition transition = this->parent_->last_transition.value(); | ||
if (this->from_state_.has_value() && this->from_state_.value(x...) != transition.from_state) | ||
return false; | ||
if (this->input_.has_value() && this->input_.value(x...) != transition.input) | ||
return false; | ||
if (this->to_state_.has_value() && this->to_state_.value(x...) != transition.to_state) | ||
return false; | ||
return true; | ||
} | ||
|
||
protected: | ||
StateMachineTextSensor *parent_; | ||
}; | ||
|
||
} // namespace esphome |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
#include "esphome/core/log.h" | ||
#include "state_machine.h" | ||
|
||
namespace esphome | ||
{ | ||
|
||
static const char *const TAG = "state_machine"; | ||
|
||
StateMachine::StateMachine( | ||
std::vector<std::string> states, | ||
std::vector<std::string> inputs, | ||
std::vector<StateTransition> transitions, | ||
std::string initial_state) | ||
{ | ||
this->states_ = states; | ||
this->inputs_ = inputs; | ||
this->transitions_ = transitions; | ||
this->current_state_ = initial_state; | ||
this->last_transition_ = {}; | ||
} | ||
|
||
void StateMachine::dump_config() | ||
{ | ||
ESP_LOGCONFIG(TAG, "Initial State: %s", this->current_state_.c_str()); | ||
|
||
ESP_LOGCONFIG(TAG, "States: %d", this->states_.size()); | ||
for (auto &state : this->states_) | ||
{ | ||
ESP_LOGCONFIG(TAG, " %s", state.c_str()); | ||
} | ||
|
||
ESP_LOGCONFIG(TAG, "Inputs: %d", this->inputs_.size()); | ||
for (auto &input : this->inputs_) | ||
{ | ||
ESP_LOGCONFIG(TAG, " %s", input.c_str()); | ||
} | ||
|
||
ESP_LOGCONFIG(TAG, "Transitions: %d", this->transitions_.size()); | ||
for (StateTransition &transition : this->transitions_) | ||
{ | ||
ESP_LOGCONFIG(TAG, " %s: %s -> %s", transition.input.c_str(), transition.from_state.c_str(), transition.to_state.c_str()); | ||
} | ||
} | ||
|
||
optional<StateTransition> StateMachine::get_transition(std::string input) | ||
{ | ||
if (std::find(this->inputs_.begin(), this->inputs_.end(), input) == this->inputs_.end()) | ||
{ | ||
ESP_LOGE(TAG, "Invalid input value: %s", input.c_str()); | ||
return {}; | ||
} | ||
|
||
for (StateTransition &transition : this->transitions_) | ||
{ | ||
if (transition.from_state == this->current_state_ && transition.input == input) | ||
return transition; | ||
} | ||
|
||
return {}; | ||
} | ||
|
||
optional<StateTransition> StateMachine::transition(std::string input) | ||
{ | ||
optional<StateTransition> transition = get_transition(input); | ||
if (transition) | ||
{ | ||
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; | ||
} | ||
else | ||
{ | ||
ESP_LOGW(TAG, "%s: invalid input %s", input.c_str(), this->current_state_.c_str()); | ||
} | ||
return transition; | ||
} | ||
|
||
} // namespace esphome |
Oops, something went wrong.