diff --git a/esphome/components/slow_pwm/output.py b/esphome/components/slow_pwm/output.py index 9cce35254b..25b091fc64 100644 --- a/esphome/components/slow_pwm/output.py +++ b/esphome/components/slow_pwm/output.py @@ -1,14 +1,13 @@ -from esphome import pins, core +from esphome import automation, core, pins +import esphome.codegen as cg from esphome.components import output import esphome.config_validation as cv -import esphome.codegen as cg -from esphome import automation from esphome.const import ( CONF_ID, - CONF_PIN, CONF_PERIOD, - CONF_TURN_ON_ACTION, + CONF_PIN, CONF_TURN_OFF_ACTION, + CONF_TURN_ON_ACTION, ) slow_pwm_ns = cg.esphome_ns.namespace("slow_pwm") @@ -16,31 +15,71 @@ SlowPWMOutput = slow_pwm_ns.class_("SlowPWMOutput", output.FloatOutput, cg.Compo CONF_STATE_CHANGE_ACTION = "state_change_action" CONF_RESTART_CYCLE_ON_STATE_CHANGE = "restart_cycle_on_state_change" +CONF_MIN_TIME_ON = "min_time_on" +CONF_MIN_TIME_OFF = "min_time_off" +CONF_MAX_PERIOD = "max_period" -CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( - { - cv.Required(CONF_ID): cv.declare_id(SlowPWMOutput), - cv.Optional(CONF_PIN): pins.gpio_output_pin_schema, - cv.Inclusive( - CONF_TURN_ON_ACTION, - "on_off", - f"{CONF_TURN_ON_ACTION} and {CONF_TURN_OFF_ACTION} must both be defined", - ): automation.validate_automation(single=True), - cv.Inclusive( - CONF_TURN_OFF_ACTION, - "on_off", - f"{CONF_TURN_ON_ACTION} and {CONF_TURN_OFF_ACTION} must both be defined", - ): automation.validate_automation(single=True), - cv.Optional(CONF_STATE_CHANGE_ACTION): automation.validate_automation( - single=True - ), - cv.Required(CONF_PERIOD): cv.All( - cv.positive_time_period_milliseconds, - cv.Range(min=core.TimePeriod(milliseconds=100)), - ), - cv.Optional(CONF_RESTART_CYCLE_ON_STATE_CHANGE, default=False): cv.boolean, - } -).extend(cv.COMPONENT_SCHEMA) + +def has_time_to_turn_on_off(config): + """Validates that the period is at least as great as min_time_on+min_time_off.""" + min_time_on = config[CONF_MIN_TIME_ON].total_milliseconds + min_time_off = config[CONF_MIN_TIME_OFF].total_milliseconds + on_off_time = core.TimePeriod(milliseconds=min_time_on + min_time_off) + + period = config[CONF_PERIOD] + max_period = config.get(CONF_MAX_PERIOD) + + if period < on_off_time: + raise cv.Invalid( + "The cycle period must be large enough to allow at least one turn on and one turn off" + ) + + if CONF_MAX_PERIOD in config and max_period: + if max_period < on_off_time: + raise cv.Invalid( + "The max cycle period must be large enough to allow at least one turn on and one turn off" + ) + + if max_period < period: + raise cv.Invalid("The max period must be larger than the default period") + + return config + + +CONFIG_SCHEMA = cv.All( + output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.Required(CONF_ID): cv.declare_id(SlowPWMOutput), + cv.Optional(CONF_PIN): pins.gpio_output_pin_schema, + cv.Inclusive( + CONF_TURN_ON_ACTION, + "on_off", + f"{CONF_TURN_ON_ACTION} and {CONF_TURN_OFF_ACTION} must both be defined", + ): automation.validate_automation(single=True), + cv.Inclusive( + CONF_TURN_OFF_ACTION, + "on_off", + f"{CONF_TURN_ON_ACTION} and {CONF_TURN_OFF_ACTION} must both be defined", + ): automation.validate_automation(single=True), + cv.Optional(CONF_STATE_CHANGE_ACTION): automation.validate_automation( + single=True + ), + cv.Required(CONF_PERIOD): cv.All( + cv.positive_time_period_milliseconds, + cv.Range(min=core.TimePeriod(milliseconds=100)), + ), + cv.Optional(CONF_RESTART_CYCLE_ON_STATE_CHANGE, default=False): cv.boolean, + cv.Optional( + CONF_MIN_TIME_ON, default="0ms" + ): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_MIN_TIME_OFF, default="0ms" + ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_MAX_PERIOD): cv.positive_time_period_milliseconds, + } + ).extend(cv.COMPONENT_SCHEMA), + has_time_to_turn_on_off, +) async def to_code(config): @@ -70,3 +109,10 @@ async def to_code(config): config[CONF_RESTART_CYCLE_ON_STATE_CHANGE] ) ) + + cg.add(var.set_min_time_on(config[CONF_MIN_TIME_ON])) + cg.add(var.set_min_time_off(config[CONF_MIN_TIME_OFF])) + + if CONF_MAX_PERIOD not in config: + config[CONF_MAX_PERIOD] = 0 + cg.add(var.set_max_period(config[CONF_MAX_PERIOD])) diff --git a/esphome/components/slow_pwm/slow_pwm_output.cpp b/esphome/components/slow_pwm/slow_pwm_output.cpp index d6b2cdfe12..41d51a8cee 100644 --- a/esphome/components/slow_pwm/slow_pwm_output.cpp +++ b/esphome/components/slow_pwm/slow_pwm_output.cpp @@ -40,11 +40,47 @@ void SlowPWMOutput::set_output_state_(bool new_state) { void SlowPWMOutput::loop() { uint32_t now = millis(); - float scaled_state = this->state_ * this->period_; + float scaled_state = this->state_ * this->current_period_; - if (now - this->period_start_time_ >= this->period_) { - ESP_LOGVV(TAG, "End of period. State: %f, Scaled state: %f", this->state_, scaled_state); - this->period_start_time_ += this->period_; + if (this->state_ > 0 && (!this->max_period_ || this->current_period_ < this->max_period_) && + (scaled_state - (float) this->min_time_on_) < -1.0) { // allow 1ms of floating point error + this->current_period_ = (unsigned int) ((float) this->min_time_on_ / this->state_); + ESP_LOGVV(TAG, "Current cycle extended to %d ms to prevent on time of just %.0f ms", this->current_period_, + scaled_state); + + scaled_state = (float) this->min_time_on_; + } + + if (this->state_ < 1.0 && (!this->max_period_ || this->current_period_ < this->max_period_) && + ((float) this->current_period_ - scaled_state - (float) this->min_time_off_) < + -1.0) { // allow 1ms of floating point error + float required_time_off = (float) this->current_period_ - scaled_state; + this->current_period_ = (unsigned int) ((float) this->min_time_off_ / (1.0 - this->state_)); + ESP_LOGVV(TAG, "Current cycle extended to %d ms to prevent off time of just %.0f ms", this->current_period_, + required_time_off); + + scaled_state = (float) (this->current_period_ - this->min_time_off_); + } + + if (this->max_period_ && this->current_period_ > this->max_period_) { + this->current_period_ = this->max_period_; + ESP_LOGVV(TAG, "Current cycle reduced to %d ms to obey max period", this->current_period_); + } + + if (this->state_ == 0) + scaled_state = 0; + else if (this->state_ == 1) + scaled_state = (float) this->current_period_; + else { + scaled_state = std::min(std::max((float) this->min_time_on_, this->state_ * this->current_period_), + (float) this->current_period_ - this->min_time_off_); + } + + if (now - this->period_start_time_ >= this->current_period_) { + ESP_LOGVV(TAG, "End of period (%d ms). State: %f, Scaled state: %f", this->current_period_, this->state_, + scaled_state); + this->period_start_time_ += this->current_period_; + this->current_period_ = this->period_; } this->set_output_state_(scaled_state > now - this->period_start_time_); @@ -64,6 +100,9 @@ void SlowPWMOutput::dump_config() { } ESP_LOGCONFIG(TAG, " Period: %d ms", this->period_); ESP_LOGCONFIG(TAG, " Restart cycle on state change: %s", YESNO(this->restart_cycle_on_state_change_)); + ESP_LOGCONFIG(TAG, " Minimum time on: %d ms", this->min_time_on_); + ESP_LOGCONFIG(TAG, " Minimum time off: %d ms", this->min_time_off_); + ESP_LOGCONFIG(TAG, " Maximum period length: %d ms", this->max_period_); LOG_FLOAT_OUTPUT(this); } diff --git a/esphome/components/slow_pwm/slow_pwm_output.h b/esphome/components/slow_pwm/slow_pwm_output.h index 3e5a3e2a40..81e7f9ff54 100644 --- a/esphome/components/slow_pwm/slow_pwm_output.h +++ b/esphome/components/slow_pwm/slow_pwm_output.h @@ -10,10 +10,16 @@ namespace slow_pwm { class SlowPWMOutput : public output::FloatOutput, public Component { public: void set_pin(GPIOPin *pin) { pin_ = pin; }; - void set_period(unsigned int period) { period_ = period; }; + void set_period(unsigned int period) { + period_ = period; + current_period_ = period; + }; void set_restart_cycle_on_state_change(bool restart_cycle_on_state_change) { restart_cycle_on_state_change_ = restart_cycle_on_state_change; } + void set_min_time_on(unsigned int min_time_on) { min_time_on_ = min_time_on; } + void set_min_time_off(unsigned int min_time_off) { min_time_off_ = min_time_off; } + void set_max_period(unsigned int max_period) { max_period_ = max_period; } void restart_cycle() { this->period_start_time_ = millis(); } /// Initialize pin @@ -55,6 +61,11 @@ class SlowPWMOutput : public output::FloatOutput, public Component { unsigned int period_start_time_{0}; unsigned int period_; bool restart_cycle_on_state_change_; + unsigned int min_time_on_; + unsigned int min_time_off_; + unsigned int max_period_; + + unsigned int current_period_; }; } // namespace slow_pwm