mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 22:53:59 +00:00 
			
		
		
		
	New script modes POC (#1168)
Co-authored-by: Guillermo Ruffino <glm.net@gmail.com>
This commit is contained in:
		| @@ -108,7 +108,7 @@ void BangBangClimate::switch_to_action_(climate::ClimateAction action) { | ||||
|   } | ||||
|  | ||||
|   if (this->prev_trigger_ != nullptr) { | ||||
|     this->prev_trigger_->stop(); | ||||
|     this->prev_trigger_->stop_action(); | ||||
|     this->prev_trigger_ = nullptr; | ||||
|   } | ||||
|   Trigger<> *trig; | ||||
|   | ||||
| @@ -94,7 +94,7 @@ void EndstopCover::dump_config() { | ||||
| float EndstopCover::get_setup_priority() const { return setup_priority::DATA; } | ||||
| void EndstopCover::stop_prev_trigger_() { | ||||
|   if (this->prev_command_trigger_ != nullptr) { | ||||
|     this->prev_command_trigger_->stop(); | ||||
|     this->prev_command_trigger_->stop_action(); | ||||
|     this->prev_command_trigger_ = nullptr; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -67,9 +67,9 @@ class LambdaLightEffect : public LightEffect { | ||||
| class AutomationLightEffect : public LightEffect { | ||||
|  public: | ||||
|   AutomationLightEffect(const std::string &name) : LightEffect(name) {} | ||||
|   void stop() override { this->trig_->stop(); } | ||||
|   void stop() override { this->trig_->stop_action(); } | ||||
|   void apply() override { | ||||
|     if (!this->trig_->is_running()) { | ||||
|     if (!this->trig_->is_action_running()) { | ||||
|       this->trig_->trigger(); | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -2,7 +2,7 @@ import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome import automation | ||||
| from esphome.automation import maybe_simple_id | ||||
| from esphome.const import CONF_ID | ||||
| from esphome.const import CONF_ID, CONF_MODE | ||||
|  | ||||
| script_ns = cg.esphome_ns.namespace('script') | ||||
| Script = script_ns.class_('Script', automation.Trigger.template()) | ||||
| @@ -10,10 +10,47 @@ ScriptExecuteAction = script_ns.class_('ScriptExecuteAction', automation.Action) | ||||
| ScriptStopAction = script_ns.class_('ScriptStopAction', automation.Action) | ||||
| ScriptWaitAction = script_ns.class_('ScriptWaitAction', automation.Action, cg.Component) | ||||
| IsRunningCondition = script_ns.class_('IsRunningCondition', automation.Condition) | ||||
| SingleScript = script_ns.class_('SingleScript', Script) | ||||
| RestartScript = script_ns.class_('RestartScript', Script) | ||||
| QueueingScript = script_ns.class_('QueueingScript', Script, cg.Component) | ||||
| ParallelScript = script_ns.class_('ParallelScript', Script) | ||||
|  | ||||
| CONF_SINGLE = 'single' | ||||
| CONF_RESTART = 'restart' | ||||
| CONF_QUEUE = 'queue' | ||||
| CONF_PARALLEL = 'parallel' | ||||
| CONF_MAX_RUNS = 'max_runs' | ||||
|  | ||||
| SCRIPT_MODES = { | ||||
|     CONF_SINGLE: SingleScript, | ||||
|     CONF_RESTART: RestartScript, | ||||
|     CONF_QUEUE: QueueingScript, | ||||
|     CONF_PARALLEL: ParallelScript, | ||||
| } | ||||
|  | ||||
|  | ||||
| def check_max_runs(value): | ||||
|     if CONF_MAX_RUNS not in value: | ||||
|         return value | ||||
|     if value[CONF_MODE] not in [CONF_QUEUE, CONF_PARALLEL]: | ||||
|         raise cv.Invalid("The option 'max_runs' is only valid in 'queue' and 'parallel' mode.", | ||||
|                          path=[CONF_MAX_RUNS]) | ||||
|     return value | ||||
|  | ||||
|  | ||||
| def assign_declare_id(value): | ||||
|     value = value.copy() | ||||
|     value[CONF_ID] = cv.declare_id(SCRIPT_MODES[value[CONF_MODE]])(value[CONF_ID]) | ||||
|     return value | ||||
|  | ||||
|  | ||||
| CONFIG_SCHEMA = automation.validate_automation({ | ||||
|     cv.Required(CONF_ID): cv.declare_id(Script), | ||||
| }) | ||||
|     # Don't declare id as cv.declare_id yet, because the ID type | ||||
|     # dpeends on the mode. Will be checked later with assign_declare_id | ||||
|     cv.Required(CONF_ID): cv.string_strict, | ||||
|     cv.Optional(CONF_MODE, default=CONF_SINGLE): cv.one_of(*SCRIPT_MODES, lower=True), | ||||
|     cv.Optional(CONF_MAX_RUNS): cv.positive_int, | ||||
| }, extra_validators=cv.All(check_max_runs, assign_declare_id)) | ||||
|  | ||||
|  | ||||
| def to_code(config): | ||||
| @@ -21,6 +58,15 @@ def to_code(config): | ||||
|     triggers = [] | ||||
|     for conf in config: | ||||
|         trigger = cg.new_Pvariable(conf[CONF_ID]) | ||||
|         # Add a human-readable name to the script | ||||
|         cg.add(trigger.set_name(conf[CONF_ID].id)) | ||||
|  | ||||
|         if CONF_MAX_RUNS in conf: | ||||
|             cg.add(trigger.set_max_runs(conf[CONF_MAX_RUNS])) | ||||
|  | ||||
|         if conf[CONF_MODE] == CONF_QUEUE: | ||||
|             yield cg.register_component(trigger, conf) | ||||
|  | ||||
|         triggers.append((trigger, conf)) | ||||
|  | ||||
|     for trigger, conf in triggers: | ||||
|   | ||||
							
								
								
									
										67
									
								
								esphome/components/script/script.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								esphome/components/script/script.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | ||||
| #include "script.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace script { | ||||
|  | ||||
| static const char *TAG = "script"; | ||||
|  | ||||
| void SingleScript::execute() { | ||||
|   if (this->is_action_running()) { | ||||
|     ESP_LOGW(TAG, "Script '%s' is already running! (mode: single)", this->name_.c_str()); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   this->trigger(); | ||||
| } | ||||
|  | ||||
| void RestartScript::execute() { | ||||
|   if (this->is_action_running()) { | ||||
|     ESP_LOGD(TAG, "Script '%s' restarting (mode: restart)", this->name_.c_str()); | ||||
|     this->stop_action(); | ||||
|   } | ||||
|  | ||||
|   this->trigger(); | ||||
| } | ||||
|  | ||||
| void QueueingScript::execute() { | ||||
|   if (this->is_action_running()) { | ||||
|     // num_runs_ is the number of *queued* instances, so total number of instances is | ||||
|     // num_runs_ + 1 | ||||
|     if (this->max_runs_ != 0 && this->num_runs_ + 1 >= this->max_runs_) { | ||||
|       ESP_LOGW(TAG, "Script '%s' maximum number of queued runs exceeded!", this->name_.c_str()); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     ESP_LOGD(TAG, "Script '%s' queueing new instance (mode: queue)", this->name_.c_str()); | ||||
|     this->num_runs_++; | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   this->trigger(); | ||||
|   // Check if the trigger was immediate and we can continue right away. | ||||
|   this->loop(); | ||||
| } | ||||
|  | ||||
| void QueueingScript::stop() { | ||||
|   this->num_runs_ = 0; | ||||
|   Script::stop(); | ||||
| } | ||||
|  | ||||
| void QueueingScript::loop() { | ||||
|   if (this->num_runs_ != 0 && !this->is_action_running()) { | ||||
|     this->num_runs_--; | ||||
|     this->trigger(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void ParallelScript::execute() { | ||||
|   if (this->max_runs_ != 0 && this->automation_parent_->num_running() >= this->max_runs_) { | ||||
|     ESP_LOGW(TAG, "Script '%s' maximum number of parallel runs exceeded!", this->name_.c_str()); | ||||
|     return; | ||||
|   } | ||||
|   this->trigger(); | ||||
| } | ||||
|  | ||||
| }  // namespace script | ||||
| }  // namespace esphome | ||||
| @@ -1,29 +1,86 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/automation.h" | ||||
| #include "esphome/core/component.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace script { | ||||
|  | ||||
| /// The abstract base class for all script types. | ||||
| class Script : public Trigger<> { | ||||
|  public: | ||||
|   void execute() { | ||||
|     bool prev = this->in_stack_; | ||||
|     this->in_stack_ = true; | ||||
|     this->trigger(); | ||||
|     this->in_stack_ = prev; | ||||
|   } | ||||
|   bool script_is_running() { return this->in_stack_ || this->is_running(); } | ||||
|   /** Execute a new instance of this script. | ||||
|    * | ||||
|    * The behavior of this function when a script is already running is defined by the subtypes | ||||
|    */ | ||||
|   virtual void execute() = 0; | ||||
|   /// Check if any instance of this script is currently running. | ||||
|   virtual bool is_running() { return this->is_action_running(); } | ||||
|   /// Stop all instances of this script. | ||||
|   virtual void stop() { this->stop_action(); } | ||||
|  | ||||
|   // Internal function to give scripts readable names. | ||||
|   void set_name(const std::string &name) { name_ = name; } | ||||
|  | ||||
|  protected: | ||||
|   bool in_stack_{false}; | ||||
|   std::string name_; | ||||
| }; | ||||
|  | ||||
| /** A script type for which only a single instance at a time is allowed. | ||||
|  * | ||||
|  * If a new instance is executed while the previous one hasn't finished yet, | ||||
|  * a warning is printed and the new instance is discarded. | ||||
|  */ | ||||
| class SingleScript : public Script { | ||||
|  public: | ||||
|   void execute() override; | ||||
| }; | ||||
|  | ||||
| /** A script type that restarts scripts from the beginning when a new instance is started. | ||||
|  * | ||||
|  * If a new instance is started but another one is already running, the existing | ||||
|  * script is stopped and the new instance starts from the beginning. | ||||
|  */ | ||||
| class RestartScript : public Script { | ||||
|  public: | ||||
|   void execute() override; | ||||
| }; | ||||
|  | ||||
| /** A script type that queues new instances that are created. | ||||
|  * | ||||
|  * Only one instance of the script can be active at a time. | ||||
|  */ | ||||
| class QueueingScript : public Script, public Component { | ||||
|  public: | ||||
|   void execute() override; | ||||
|   void stop() override; | ||||
|   void loop() override; | ||||
|   void set_max_runs(int max_runs) { max_runs_ = max_runs; } | ||||
|  | ||||
|  protected: | ||||
|   int num_runs_ = 0; | ||||
|   int max_runs_ = 0; | ||||
| }; | ||||
|  | ||||
| /** A script type that executes new instances in parallel. | ||||
|  * | ||||
|  * If a new instance is started while previous ones haven't finished yet, | ||||
|  * the new one is exeucted in parallel to the other instances. | ||||
|  */ | ||||
| class ParallelScript : public Script { | ||||
|  public: | ||||
|   void execute() override; | ||||
|   void set_max_runs(int max_runs) { max_runs_ = max_runs; } | ||||
|  | ||||
|  protected: | ||||
|   int max_runs_ = 0; | ||||
| }; | ||||
|  | ||||
| template<typename... Ts> class ScriptExecuteAction : public Action<Ts...> { | ||||
|  public: | ||||
|   ScriptExecuteAction(Script *script) : script_(script) {} | ||||
|  | ||||
|   void play(Ts... x) override { this->script_->trigger(); } | ||||
|   void play(Ts... x) override { this->script_->execute(); } | ||||
|  | ||||
|  protected: | ||||
|   Script *script_; | ||||
| @@ -43,7 +100,7 @@ template<typename... Ts> class IsRunningCondition : public Condition<Ts...> { | ||||
|  public: | ||||
|   explicit IsRunningCondition(Script *parent) : parent_(parent) {} | ||||
|  | ||||
|   bool check(Ts... x) override { return this->parent_->script_is_running(); } | ||||
|   bool check(Ts... x) override { return this->parent_->is_running(); } | ||||
|  | ||||
|  protected: | ||||
|   Script *parent_; | ||||
|   | ||||
| @@ -120,7 +120,7 @@ void TemplateCover::set_has_position(bool has_position) { this->has_position_ = | ||||
| void TemplateCover::set_has_tilt(bool has_tilt) { this->has_tilt_ = has_tilt; } | ||||
| void TemplateCover::stop_prev_trigger_() { | ||||
|   if (this->prev_command_trigger_ != nullptr) { | ||||
|     this->prev_command_trigger_->stop(); | ||||
|     this->prev_command_trigger_->stop_action(); | ||||
|     this->prev_command_trigger_ = nullptr; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -19,7 +19,7 @@ void TemplateSwitch::loop() { | ||||
| } | ||||
| void TemplateSwitch::write_state(bool state) { | ||||
|   if (this->prev_trigger_ != nullptr) { | ||||
|     this->prev_trigger_->stop(); | ||||
|     this->prev_trigger_->stop_action(); | ||||
|   } | ||||
|  | ||||
|   if (state) { | ||||
|   | ||||
| @@ -223,7 +223,7 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action) { | ||||
|   } | ||||
|  | ||||
|   if (this->prev_action_trigger_ != nullptr) { | ||||
|     this->prev_action_trigger_->stop(); | ||||
|     this->prev_action_trigger_->stop_action(); | ||||
|     this->prev_action_trigger_ = nullptr; | ||||
|   } | ||||
|   Trigger<> *trig = this->idle_action_trigger_; | ||||
| @@ -262,7 +262,7 @@ void ThermostatClimate::switch_to_fan_mode_(climate::ClimateFanMode fan_mode) { | ||||
|     return; | ||||
|  | ||||
|   if (this->prev_fan_mode_trigger_ != nullptr) { | ||||
|     this->prev_fan_mode_trigger_->stop(); | ||||
|     this->prev_fan_mode_trigger_->stop_action(); | ||||
|     this->prev_fan_mode_trigger_ = nullptr; | ||||
|   } | ||||
|   Trigger<> *trig = this->fan_mode_auto_trigger_; | ||||
| @@ -313,7 +313,7 @@ void ThermostatClimate::switch_to_mode_(climate::ClimateMode mode) { | ||||
|     return; | ||||
|  | ||||
|   if (this->prev_mode_trigger_ != nullptr) { | ||||
|     this->prev_mode_trigger_->stop(); | ||||
|     this->prev_mode_trigger_->stop_action(); | ||||
|     this->prev_mode_trigger_ = nullptr; | ||||
|   } | ||||
|   Trigger<> *trig = this->auto_mode_trigger_; | ||||
| @@ -355,7 +355,7 @@ void ThermostatClimate::switch_to_swing_mode_(climate::ClimateSwingMode swing_mo | ||||
|     return; | ||||
|  | ||||
|   if (this->prev_swing_mode_trigger_ != nullptr) { | ||||
|     this->prev_swing_mode_trigger_->stop(); | ||||
|     this->prev_swing_mode_trigger_->stop_action(); | ||||
|     this->prev_swing_mode_trigger_ = nullptr; | ||||
|   } | ||||
|   Trigger<> *trig = this->swing_mode_off_trigger_; | ||||
|   | ||||
| @@ -78,7 +78,7 @@ void TimeBasedCover::control(const CoverCall &call) { | ||||
| } | ||||
| void TimeBasedCover::stop_prev_trigger_() { | ||||
|   if (this->prev_command_trigger_ != nullptr) { | ||||
|     this->prev_command_trigger_->stop(); | ||||
|     this->prev_command_trigger_->stop_action(); | ||||
|     this->prev_command_trigger_ = nullptr; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -812,6 +812,7 @@ def mqtt_qos(value): | ||||
| def requires_component(comp): | ||||
|     """Validate that this option can only be specified when the component `comp` is loaded.""" | ||||
|     def validator(value): | ||||
|         # pylint: disable=unsupported-membership-test | ||||
|         if comp not in CORE.raw_config: | ||||
|             raise Invalid(f"This option requires component {comp}") | ||||
|         return value | ||||
| @@ -1125,7 +1126,7 @@ def typed_schema(schemas, **kwargs): | ||||
|     def validator(value): | ||||
|         if not isinstance(value, dict): | ||||
|             raise Invalid("Value must be dict") | ||||
|         if CONF_TYPE not in value: | ||||
|         if key not in value: | ||||
|             raise Invalid("type not specified!") | ||||
|         value = value.copy() | ||||
|         key_v = key_validator(value.pop(key)) | ||||
| @@ -1175,6 +1176,7 @@ class OnlyWith(Optional): | ||||
|  | ||||
|     @property | ||||
|     def default(self): | ||||
|         # pylint: disable=unsupported-membership-test | ||||
|         if self._component not in CORE.raw_config: | ||||
|             return vol.UNDEFINED | ||||
|         return self._default | ||||
|   | ||||
| @@ -50,18 +50,22 @@ template<typename... Ts> class Automation; | ||||
|  | ||||
| template<typename... Ts> class Trigger { | ||||
|  public: | ||||
|   /// Inform the parent automation that the event has triggered. | ||||
|   void trigger(Ts... x) { | ||||
|     if (this->automation_parent_ == nullptr) | ||||
|       return; | ||||
|     this->automation_parent_->trigger(x...); | ||||
|   } | ||||
|   void set_automation_parent(Automation<Ts...> *automation_parent) { this->automation_parent_ = automation_parent; } | ||||
|   void stop() { | ||||
|  | ||||
|   /// Stop any action connected to this trigger. | ||||
|   void stop_action() { | ||||
|     if (this->automation_parent_ == nullptr) | ||||
|       return; | ||||
|     this->automation_parent_->stop(); | ||||
|   } | ||||
|   bool is_running() { | ||||
|   /// Returns true if any action connected to this trigger is running. | ||||
|   bool is_action_running() { | ||||
|     if (this->automation_parent_ == nullptr) | ||||
|       return false; | ||||
|     return this->automation_parent_->is_running(); | ||||
| @@ -87,8 +91,18 @@ template<typename... Ts> class Action { | ||||
|     } | ||||
|     this->stop_next_(); | ||||
|   } | ||||
|   /// Check if this or any of the following actions are currently running. | ||||
|   virtual bool is_running() { return this->num_running_ > 0 || this->is_running_next_(); } | ||||
|  | ||||
|   /// The total number of actions that are currently running in this plus any of | ||||
|   /// the following actions in the chain. | ||||
|   int num_running_total() { | ||||
|     int total = this->num_running_; | ||||
|     if (this->next_ != nullptr) | ||||
|       total += this->next_->num_running_total(); | ||||
|     return total; | ||||
|   } | ||||
|  | ||||
|  protected: | ||||
|   friend ActionList<Ts...>; | ||||
|  | ||||
| @@ -123,6 +137,8 @@ template<typename... Ts> class Action { | ||||
|  | ||||
|   Action<Ts...> *next_ = nullptr; | ||||
|  | ||||
|   /// The number of instances of this sequence in the list of actions | ||||
|   /// that is currently being executed. | ||||
|   int num_running_{0}; | ||||
| }; | ||||
|  | ||||
| @@ -151,11 +167,19 @@ template<typename... Ts> class ActionList { | ||||
|       this->actions_begin_->stop_complex(); | ||||
|   } | ||||
|   bool empty() const { return this->actions_begin_ == nullptr; } | ||||
|  | ||||
|   /// Check if any action in this action list is currently running. | ||||
|   bool is_running() { | ||||
|     if (this->actions_begin_ == nullptr) | ||||
|       return false; | ||||
|     return this->actions_begin_->is_running(); | ||||
|   } | ||||
|   /// Return the number of actions in this action list that are currently running. | ||||
|   int num_running() { | ||||
|     if (this->actions_begin_ == nullptr) | ||||
|       return false; | ||||
|     return this->actions_begin_->num_running_total(); | ||||
|   } | ||||
|  | ||||
|  protected: | ||||
|   template<int... S> void play_tuple_(const std::tuple<Ts...> &tuple, seq<S...>) { this->play(std::get<S>(tuple)...); } | ||||
| @@ -177,6 +201,9 @@ template<typename... Ts> class Automation { | ||||
|  | ||||
|   bool is_running() { return this->actions_.is_running(); } | ||||
|  | ||||
|   /// Return the number of actions in the action part of this automation that are currently running. | ||||
|   int num_running() { return this->actions_.num_running(); } | ||||
|  | ||||
|  protected: | ||||
|   Trigger<Ts...> *trigger_; | ||||
|   ActionList<Ts...> actions_; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user