Skip to content

Commit 6815331

Browse files
authored
Merge pull request #10 from muxa/dev
1.0.0-rc.6
2 parents 55d88f9 + b9d6613 commit 6815331

9 files changed

+504
-5
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,4 @@ __pycache__
3535
toggle_example
3636
.esphome
3737
dimmable_light_example
38+
dual-switch-cover-example

README.md

+34
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ binary_sensor:
6161
* **name** (**Required**, string): The name of the state. Must not repeat.
6262
* **on_enter** (*Optional*, [Automation](https://esphome.io/guides/automations.html#automation)): An automation to perform when entering this state.
6363
* **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.
64+
* **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.
6465

6566
* **inputs** (**Required**, list): The list of inputs that the state machine supports with allowed state transitions.
6667

@@ -97,6 +98,29 @@ Configuration options:
9798
* **id** (*Optional*, [ID](https://esphome.io/guides/configuration-types.html#config-id)): The ID of the state machine.
9899
* **input** (**Required**, string): The input to provide in order to transition to the next state.
99100

101+
## `state_machine.set` Action
102+
103+
This action allows resetting the state machine current state, without going through transitions. This can be useful when initial state is not really know until some sensor data is available. Another example is when there's a "emergency escape" transition from every state to a particular state and adding a transition from every other state just makes the machine messy.
104+
105+
> Note that only the target state `on_set` automation will be triggered, and all other state machine automations (`on_enter`, `on_leave` and `action` of the inputs and transitions) will be skipped.
106+
107+
```yaml
108+
# in some trigger
109+
on_...:
110+
# Basic:
111+
- state_machine.set: OPEN
112+
113+
# Advanced (if you have multiple state machines in one YAML)
114+
- state_machine.set:
115+
id: sm1
116+
state: OPEN
117+
```
118+
119+
Configuration options:
120+
121+
* **id** (*Optional*, [ID](https://esphome.io/guides/configuration-types.html#config-id)): The ID of the state machine.
122+
* **state** (**Required**, string): The state to set state machine to bypassing transitions.
123+
100124
## `state_machine.transition` Condition
101125

102126
This condition lets you check what transition last occurred.
@@ -194,3 +218,13 @@ This example models a single button control for a dimmable light with the follow
194218

195219
See [dimmable_light_example.yaml](dimmable_light_example.yaml).
196220

221+
### Dual Switch Cover Control
222+
223+
![Dual Switch Cover Control State Machine Diagram](images/state-machine-dual-switch-cover.svg)
224+
225+
This example demonstrates using 2 switches to control a time-based cover:
226+
* Switch 1 ON: OPEN
227+
* Switch 2 ON: CLOSE
228+
* Both switches ON: enable Sun Tracker
229+
230+
See [dual-switch-cover-example.yaml](dual-switch-cover-example.yaml).

components/state_machine/__init__.py

+41-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@
2525
"StateMachineComponent", cg.Component
2626
)
2727

28+
StateMachineOnSetTrigger = state_machine_ns.class_(
29+
"StateMachineOnSetTrigger", automation.Trigger.template()
30+
)
31+
2832
StateMachineOnEnterTrigger = state_machine_ns.class_(
2933
"StateMachineOnEnterTrigger", automation.Trigger.template()
3034
)
@@ -41,6 +45,8 @@
4145
"StateMachineTransitionActionTrigger", automation.Trigger.template()
4246
)
4347

48+
StateMachineSetAction = state_machine_ns.class_("StateMachineSetAction", automation.Action)
49+
4450
StateMachineTransitionAction = state_machine_ns.class_("StateMachineTransitionAction", automation.Action)
4551

4652
StateMachineTransitionCondition = state_machine_ns.class_("StateMachineTransitionCondition", automation.Condition)
@@ -51,6 +57,7 @@
5157
CONF_INPUTS_KEY = 'inputs'
5258
CONF_TRANSITIONS_KEY = 'transitions'
5359

60+
CONF_STATE_ON_SET_KEY = 'on_set'
5461
CONF_STATE_ON_ENTER_KEY = 'on_enter'
5562
CONF_STATE_ON_LEAVE_KEY = 'on_leave'
5663
CONF_INPUT_TRANSITIONS_KEY = 'transitions'
@@ -61,6 +68,8 @@
6168
CONF_TRANSITION_INPUT_KEY = 'input'
6269
CONF_TRANSITION_TO_KEY = 'to'
6370

71+
CONF_STATE_KEY = 'state'
72+
6473
CONF_STATE_MACHINE_ID = 'state_machine_id'
6574

6675
def validate_transition(value):
@@ -137,6 +146,11 @@ def unique_names(items):
137146
cv.ensure_list(cv.maybe_simple_value(
138147
{
139148
cv.Required(CONF_NAME): cv.string_strict,
149+
cv.Optional(CONF_STATE_ON_SET_KEY): automation.validate_automation(
150+
{
151+
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateMachineOnSetTrigger),
152+
}
153+
),
140154
cv.Optional(CONF_STATE_ON_ENTER_KEY): automation.validate_automation(
141155
{
142156
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateMachineOnEnterTrigger),
@@ -221,7 +235,17 @@ async def to_code(config):
221235
if CONF_NAME in config:
222236
cg.add(var.set_name(config[CONF_NAME]))
223237

224-
# setup on_leave automations first (to ensure they are executed before on_enter)
238+
# setup on_set automations first
239+
for state in config[CONF_STATES_KEY]:
240+
241+
if CONF_STATE_ON_LEAVE_KEY in state:
242+
for action in state.get(CONF_STATE_ON_SET_KEY, []):
243+
trigger = cg.new_Pvariable(
244+
action[CONF_TRIGGER_ID], var, state[CONF_NAME]
245+
)
246+
await automation.build_automation(trigger, [], action)
247+
248+
# then setup on_leave automations (to ensure they are executed before on_enter)
225249
for state in config[CONF_STATES_KEY]:
226250

227251
if CONF_STATE_ON_LEAVE_KEY in state:
@@ -270,6 +294,22 @@ async def to_code(config):
270294

271295
cg.add(var.dump_config())
272296

297+
@automation.register_action(
298+
"state_machine.set",
299+
StateMachineSetAction,
300+
cv.maybe_simple_value(
301+
{
302+
cv.GenerateID(): cv.use_id(StateMachineComponent),
303+
cv.Required(CONF_STATE_KEY): cv.string_strict,
304+
},
305+
key=CONF_STATE_KEY
306+
),
307+
)
308+
def state_machine_set_to_code(config, action_id, template_arg, args):
309+
var = cg.new_Pvariable(action_id, template_arg, config[CONF_STATE_KEY])
310+
yield cg.register_parented(var, config[CONF_ID])
311+
yield var
312+
273313
@automation.register_action(
274314
"state_machine.transition",
275315
StateMachineTransitionAction,

components/state_machine/automation.h

+32
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,23 @@ namespace esphome
99
namespace state_machine
1010
{
1111

12+
class StateMachineOnSetTrigger : public Trigger<>
13+
{
14+
public:
15+
StateMachineOnSetTrigger(StateMachineComponent *state_machine, std::string state)
16+
{
17+
state_machine->add_on_set_callback(
18+
[this, state](std::string new_state)
19+
{
20+
if (new_state == state)
21+
{
22+
this->stop_action(); // stop any previous running actions
23+
this->trigger();
24+
}
25+
});
26+
}
27+
};
28+
1229
class StateMachineOnEnterTrigger : public Trigger<>
1330
{
1431
public:
@@ -77,6 +94,21 @@ namespace esphome
7794
}
7895
};
7996

97+
template <typename... Ts>
98+
class StateMachineSetAction : public Action<Ts...>, public Parented<StateMachineComponent>
99+
{
100+
public:
101+
StateMachineSetAction(std::string state)
102+
{
103+
this->state_ = state;
104+
}
105+
106+
void play(Ts... x) override { this->parent_->set(this->state_); }
107+
108+
protected:
109+
std::string state_;
110+
};
111+
80112
template <typename... Ts>
81113
class StateMachineTransitionAction : public Action<Ts...>, public Parented<StateMachineComponent>
82114
{

components/state_machine/state_machine.cpp

+23-1
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,18 @@ namespace esphome
2727
this->name_ = name;
2828
}
2929

30+
void StateMachineComponent::setup()
31+
{
32+
auto initial_state = this->current_state_;
33+
this->current_state_ = {};
34+
this->set(initial_state);
35+
}
36+
3037
void StateMachineComponent::dump_config()
3138
{
3239
ESP_LOGCONFIG(TAG, "State Machine '%s'", this->name_.c_str());
3340

34-
ESP_LOGCONFIG(TAG, "Initial State: %s", this->current_state_.c_str());
41+
ESP_LOGCONFIG(TAG, "Current State: %s", this->current_state_.c_str());
3542

3643
ESP_LOGCONFIG(TAG, "States: %d", this->states_.size());
3744
for (auto &state : this->states_)
@@ -69,6 +76,21 @@ namespace esphome
6976
return {};
7077
}
7178

79+
void StateMachineComponent::set(std::string state)
80+
{
81+
if (std::find(this->states_.begin(), this->states_.end(), state) == this->states_.end())
82+
{
83+
ESP_LOGE(TAG, "Invalid state: %s", state.c_str());
84+
}
85+
86+
if (state != this->current_state_)
87+
{
88+
ESP_LOGD(TAG, "State set to %s", state.c_str());
89+
this->current_state_ = state;
90+
this->set_callback_.call(state);
91+
}
92+
}
93+
7294
optional<StateTransition> StateMachineComponent::transition(std::string input)
7395
{
7496
optional<StateTransition> transition = this->get_transition(input);

components/state_machine/state_machine.h

+4
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,16 @@ namespace esphome
2626
const std::string &get_name() const;
2727
void set_name(const std::string &name);
2828

29+
void setup() override;
2930
void dump_config() override;
3031

3132
std::string current_state() { return this->current_state_; }
3233
optional<StateTransition> last_transition() { return this->last_transition_; }
3334

35+
void set(std::string state);
3436
optional<StateTransition> transition(std::string input);
3537

38+
void add_on_set_callback(std::function<void(std::string)> &&callback) { this->set_callback_.add(std::move(callback)); }
3639
void add_on_transition_callback(std::function<void(StateTransition)> &&callback) { this->transition_callback_.add(std::move(callback)); }
3740

3841
protected:
@@ -46,6 +49,7 @@ namespace esphome
4649

4750
optional<StateTransition> get_transition(std::string input);
4851

52+
CallbackManager<void(std::string)> set_callback_{};
4953
CallbackManager<void(StateTransition)> transition_callback_{};
5054
};
5155

dimmable_light_example.yaml

+31-3
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,28 @@ state_machine:
3939
call.perform();
4040
- name: HOLD
4141
transitions:
42-
- ON -> EDITING
43-
- EDITING -> ON
42+
- from: "ON"
43+
to: EDITING
44+
action: # single flash to indicate beginning of editing
45+
- light.turn_on:
46+
id: light1
47+
effect: Strobe
48+
- delay: 0.2s
49+
- light.turn_off: light1
50+
- light.turn_on:
51+
id: light1
52+
transition_length: 100ms
53+
- from: EDITING
54+
to: "ON"
55+
action: # tripple flash to indicate end of editing
56+
- light.turn_on:
57+
id: light1
58+
effect: Strobe
59+
- delay: 0.6s
60+
- light.turn_off: light1
61+
- light.turn_on:
62+
id: light1
63+
transition_length: 100ms
4464
diagram: true
4565

4666
binary_sensor:
@@ -65,8 +85,16 @@ light:
6585
- platform: monochromatic
6686
name: "Light"
6787
id: light1
68-
default_transition_length: 300ms
88+
default_transition_length: 200ms
6989
output: led1
90+
effects:
91+
- strobe:
92+
name: Strobe
93+
colors:
94+
- state: true
95+
duration: 100ms
96+
- state: false
97+
duration: 100ms
7098

7199
output:
72100
- platform: esp8266_pwm

0 commit comments

Comments
 (0)