From 98b3d294aab333e73c92117bbb38e25969d73c6c Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Tue, 21 Feb 2023 19:47:50 -0600 Subject: [PATCH] Sprinkler "v2" updates (#4159) * Add standby switch * Add support for arbitrary run duration in start_single_valve action * Add divider feature * Allow zero multiplier * Fixes for #3740, misc. cleanup and polishing * Integrate number components for multiplier, repeat and run duration * Add various methods to get time remaining * Add next_prev_ignore_disabled flag * Optimize next/previous valve selection methods * Add numbers_use_minutes flag * Initialize switch states as they are set up * Ensure SprinklerControllerSwitch has state if it's not restored * Add repeat validation * Misc. clean-up and tweaking * Fix bugprone-integer-division * More clean-up * Set entity_category for standby_switch * Set default entity_category for numbers * More housekeeping * Add run request tracking * Fix time remaining calculation * Use native unit_of_measurement for run duration numbers * Unstack some ifs --- esphome/components/sprinkler/__init__.py | 264 ++++++++- esphome/components/sprinkler/automation.h | 18 +- esphome/components/sprinkler/sprinkler.cpp | 633 ++++++++++++++++----- esphome/components/sprinkler/sprinkler.h | 115 +++- 4 files changed, 854 insertions(+), 176 deletions(-) diff --git a/esphome/components/sprinkler/__init__.py b/esphome/components/sprinkler/__init__.py index 52de290c85..cf3f471234 100644 --- a/esphome/components/sprinkler/__init__.py +++ b/esphome/components/sprinkler/__init__.py @@ -2,23 +2,36 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation from esphome.automation import maybe_simple_id +from esphome.components import number from esphome.components import switch from esphome.const import ( + CONF_ENTITY_CATEGORY, CONF_ID, + CONF_INITIAL_VALUE, + CONF_MAX_VALUE, + CONF_MIN_VALUE, CONF_NAME, CONF_REPEAT, + CONF_RESTORE_VALUE, CONF_RUN_DURATION, + CONF_STEP, + CONF_UNIT_OF_MEASUREMENT, ENTITY_CATEGORY_CONFIG, + UNIT_MINUTE, + UNIT_SECOND, ) -AUTO_LOAD = ["switch"] +AUTO_LOAD = ["number", "switch"] CODEOWNERS = ["@kbx81"] CONF_AUTO_ADVANCE_SWITCH = "auto_advance_switch" +CONF_DIVIDER = "divider" CONF_ENABLE_SWITCH = "enable_switch" CONF_MAIN_SWITCH = "main_switch" CONF_MANUAL_SELECTION_DELAY = "manual_selection_delay" CONF_MULTIPLIER = "multiplier" +CONF_MULTIPLIER_NUMBER = "multiplier_number" +CONF_NEXT_PREV_IGNORE_DISABLED = "next_prev_ignore_disabled" CONF_PUMP_OFF_SWITCH_ID = "pump_off_switch_id" CONF_PUMP_ON_SWITCH_ID = "pump_on_switch_id" CONF_PUMP_PULSE_DURATION = "pump_pulse_duration" @@ -30,7 +43,11 @@ CONF_PUMP_SWITCH = "pump_switch" CONF_PUMP_SWITCH_ID = "pump_switch_id" CONF_PUMP_SWITCH_OFF_DURING_VALVE_OPEN_DELAY = "pump_switch_off_during_valve_open_delay" CONF_QUEUE_ENABLE_SWITCH = "queue_enable_switch" +CONF_REPEAT_NUMBER = "repeat_number" CONF_REVERSE_SWITCH = "reverse_switch" +CONF_RUN_DURATION_NUMBER = "run_duration_number" +CONF_SET_ACTION = "set_action" +CONF_STANDBY_SWITCH = "standby_switch" CONF_VALVE_NUMBER = "valve_number" CONF_VALVE_OPEN_DELAY = "valve_open_delay" CONF_VALVE_OVERLAP = "valve_overlap" @@ -43,10 +60,14 @@ CONF_VALVES = "valves" sprinkler_ns = cg.esphome_ns.namespace("sprinkler") Sprinkler = sprinkler_ns.class_("Sprinkler", cg.Component) +SprinklerControllerNumber = sprinkler_ns.class_( + "SprinklerControllerNumber", number.Number, cg.Component +) SprinklerControllerSwitch = sprinkler_ns.class_( "SprinklerControllerSwitch", switch.Switch, cg.Component ) +SetDividerAction = sprinkler_ns.class_("SetDividerAction", automation.Action) SetMultiplierAction = sprinkler_ns.class_("SetMultiplierAction", automation.Action) QueueValveAction = sprinkler_ns.class_("QueueValveAction", automation.Action) ClearQueuedValvesAction = sprinkler_ns.class_( @@ -67,6 +88,19 @@ ResumeAction = sprinkler_ns.class_("ResumeAction", automation.Action) ResumeOrStartAction = sprinkler_ns.class_("ResumeOrStartAction", automation.Action) +def validate_min_max(config): + if config[CONF_MAX_VALUE] <= config[CONF_MIN_VALUE]: + raise cv.Invalid(f"{CONF_MAX_VALUE} must be greater than {CONF_MIN_VALUE}") + + if (config[CONF_INITIAL_VALUE] > config[CONF_MAX_VALUE]) or ( + config[CONF_INITIAL_VALUE] < config[CONF_MIN_VALUE] + ): + raise cv.Invalid( + f"{CONF_INITIAL_VALUE} must be a value between {CONF_MAX_VALUE} and {CONF_MIN_VALUE}" + ) + return config + + def validate_sprinkler(config): for sprinkler_controller_index, sprinkler_controller in enumerate(config): if len(sprinkler_controller[CONF_VALVES]) <= 1: @@ -104,9 +138,18 @@ def validate_sprinkler(config): f"{CONF_VALVE_OPEN_DELAY} must be defined when {CONF_PUMP_SWITCH_OFF_DURING_VALVE_OPEN_DELAY} is enabled" ) + if ( + CONF_REPEAT in sprinkler_controller + and CONF_REPEAT_NUMBER in sprinkler_controller + ): + raise cv.Invalid( + f"Do not specify {CONF_REPEAT} when using {CONF_REPEAT_NUMBER}; use number component's {CONF_INITIAL_VALUE} instead" + ) + for valve in sprinkler_controller[CONF_VALVES]: if ( CONF_VALVE_OVERLAP in sprinkler_controller + and CONF_RUN_DURATION in valve and valve[CONF_RUN_DURATION] <= sprinkler_controller[CONF_VALVE_OVERLAP] ): raise cv.Invalid( @@ -114,6 +157,7 @@ def validate_sprinkler(config): ) if ( CONF_VALVE_OPEN_DELAY in sprinkler_controller + and CONF_RUN_DURATION in valve and valve[CONF_RUN_DURATION] <= sprinkler_controller[CONF_VALVE_OPEN_DELAY] ): @@ -170,6 +214,14 @@ def validate_sprinkler(config): raise cv.Invalid( f"Either {CONF_VALVE_SWITCH_ID} or {CONF_VALVE_OFF_SWITCH_ID} and {CONF_VALVE_ON_SWITCH_ID} must be specified in valve configuration" ) + if CONF_RUN_DURATION not in valve and CONF_RUN_DURATION_NUMBER not in valve: + raise cv.Invalid( + f"Either {CONF_RUN_DURATION} or {CONF_RUN_DURATION_NUMBER} must be specified for each valve" + ) + if CONF_RUN_DURATION in valve and CONF_RUN_DURATION_NUMBER in valve: + raise cv.Invalid( + f"Do not specify {CONF_RUN_DURATION} when using {CONF_RUN_DURATION_NUMBER}; use number component's {CONF_INITIAL_VALUE} instead" + ) return config @@ -190,11 +242,20 @@ SPRINKLER_ACTION_REPEAT_SCHEMA = cv.maybe_simple_value( SPRINKLER_ACTION_SINGLE_VALVE_SCHEMA = cv.maybe_simple_value( { cv.GenerateID(): cv.use_id(Sprinkler), + cv.Optional(CONF_RUN_DURATION): cv.templatable(cv.positive_time_period_seconds), cv.Required(CONF_VALVE_NUMBER): cv.templatable(cv.positive_int), }, key=CONF_VALVE_NUMBER, ) +SPRINKLER_ACTION_SET_DIVIDER_SCHEMA = cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(Sprinkler), + cv.Required(CONF_DIVIDER): cv.templatable(cv.positive_int), + }, + key=CONF_DIVIDER, +) + SPRINKLER_ACTION_SET_MULTIPLIER_SCHEMA = cv.maybe_simple_value( { cv.GenerateID(): cv.use_id(Sprinkler), @@ -232,7 +293,30 @@ SPRINKLER_VALVE_SCHEMA = cv.Schema( cv.Optional(CONF_PUMP_OFF_SWITCH_ID): cv.use_id(switch.Switch), cv.Optional(CONF_PUMP_ON_SWITCH_ID): cv.use_id(switch.Switch), cv.Optional(CONF_PUMP_SWITCH_ID): cv.use_id(switch.Switch), - cv.Required(CONF_RUN_DURATION): cv.positive_time_period_seconds, + cv.Optional(CONF_RUN_DURATION): cv.positive_time_period_seconds, + cv.Optional(CONF_RUN_DURATION_NUMBER): cv.maybe_simple_value( + number.NUMBER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(SprinklerControllerNumber), + cv.Optional( + CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_CONFIG + ): cv.entity_category, + cv.Optional(CONF_INITIAL_VALUE, default=900): cv.positive_int, + cv.Optional(CONF_MAX_VALUE, default=86400): cv.positive_int, + cv.Optional(CONF_MIN_VALUE, default=1): cv.positive_int, + cv.Optional(CONF_RESTORE_VALUE, default=True): cv.boolean, + cv.Optional(CONF_STEP, default=1): cv.positive_int, + cv.Optional(CONF_SET_ACTION): automation.validate_automation( + single=True + ), + cv.Optional( + CONF_UNIT_OF_MEASUREMENT, default=UNIT_SECOND + ): cv.one_of(UNIT_MINUTE, UNIT_SECOND, lower="True"), + } + ).extend(cv.COMPONENT_SCHEMA), + validate_min_max, + key=CONF_NAME, + ), cv.Required(CONF_VALVE_SWITCH): cv.maybe_simple_value( switch.switch_schema(SprinklerControllerSwitch), key=CONF_NAME, @@ -268,8 +352,55 @@ SPRINKLER_CONTROLLER_SCHEMA = cv.Schema( ), key=CONF_NAME, ), + cv.Optional(CONF_STANDBY_SWITCH): cv.maybe_simple_value( + switch.switch_schema( + SprinklerControllerSwitch, entity_category=ENTITY_CATEGORY_CONFIG + ), + key=CONF_NAME, + ), + cv.Optional(CONF_NEXT_PREV_IGNORE_DISABLED, default=False): cv.boolean, cv.Optional(CONF_MANUAL_SELECTION_DELAY): cv.positive_time_period_seconds, + cv.Optional(CONF_MULTIPLIER_NUMBER): cv.maybe_simple_value( + number.NUMBER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(SprinklerControllerNumber), + cv.Optional( + CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_CONFIG + ): cv.entity_category, + cv.Optional(CONF_INITIAL_VALUE, default=1): cv.positive_float, + cv.Optional(CONF_MAX_VALUE, default=10): cv.positive_float, + cv.Optional(CONF_MIN_VALUE, default=0): cv.positive_float, + cv.Optional(CONF_RESTORE_VALUE, default=True): cv.boolean, + cv.Optional(CONF_STEP, default=0.1): cv.positive_float, + cv.Optional(CONF_SET_ACTION): automation.validate_automation( + single=True + ), + } + ).extend(cv.COMPONENT_SCHEMA), + validate_min_max, + key=CONF_NAME, + ), cv.Optional(CONF_REPEAT): cv.positive_int, + cv.Optional(CONF_REPEAT_NUMBER): cv.maybe_simple_value( + number.NUMBER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(SprinklerControllerNumber), + cv.Optional( + CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_CONFIG + ): cv.entity_category, + cv.Optional(CONF_INITIAL_VALUE, default=0): cv.positive_int, + cv.Optional(CONF_MAX_VALUE, default=10): cv.positive_int, + cv.Optional(CONF_MIN_VALUE, default=0): cv.positive_int, + cv.Optional(CONF_RESTORE_VALUE, default=True): cv.boolean, + cv.Optional(CONF_STEP, default=1): cv.positive_int, + cv.Optional(CONF_SET_ACTION): automation.validate_automation( + single=True + ), + } + ).extend(cv.COMPONENT_SCHEMA), + validate_min_max, + key=CONF_NAME, + ), cv.Optional(CONF_PUMP_PULSE_DURATION): cv.positive_time_period_milliseconds, cv.Optional(CONF_VALVE_PULSE_DURATION): cv.positive_time_period_milliseconds, cv.Exclusive( @@ -301,6 +432,19 @@ CONFIG_SCHEMA = cv.All( ) +@automation.register_action( + "sprinkler.set_divider", + SetDividerAction, + SPRINKLER_ACTION_SET_DIVIDER_SCHEMA, +) +async def sprinkler_set_divider_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = await cg.templatable(config[CONF_DIVIDER], args, cg.float_) + cg.add(var.set_divider(template_)) + return var + + @automation.register_action( "sprinkler.set_multiplier", SetMultiplierAction, @@ -385,6 +529,9 @@ async def sprinkler_start_single_valve_to_code(config, action_id, template_arg, var = cg.new_Pvariable(action_id, template_arg, paren) template_ = await cg.templatable(config[CONF_VALVE_NUMBER], args, cg.uint8) cg.add(var.set_valve_to_start(template_)) + if CONF_RUN_DURATION in config: + template_ = await cg.templatable(config[CONF_RUN_DURATION], args, cg.uint32) + cg.add(var.set_valve_run_duration(template_)) return var @@ -455,6 +602,79 @@ async def to_code(config): ) cg.add(var.set_controller_reverse_switch(sw_rev_var)) + if CONF_STANDBY_SWITCH in sprinkler_controller: + sw_stb_var = await switch.new_switch( + sprinkler_controller[CONF_STANDBY_SWITCH] + ) + await cg.register_component( + sw_stb_var, sprinkler_controller[CONF_STANDBY_SWITCH] + ) + cg.add(var.set_controller_standby_switch(sw_stb_var)) + + if CONF_MULTIPLIER_NUMBER in sprinkler_controller: + num_mult_var = await number.new_number( + sprinkler_controller[CONF_MULTIPLIER_NUMBER], + min_value=sprinkler_controller[CONF_MULTIPLIER_NUMBER][ + CONF_MIN_VALUE + ], + max_value=sprinkler_controller[CONF_MULTIPLIER_NUMBER][ + CONF_MAX_VALUE + ], + step=sprinkler_controller[CONF_MULTIPLIER_NUMBER][CONF_STEP], + ) + await cg.register_component( + num_mult_var, sprinkler_controller[CONF_MULTIPLIER_NUMBER] + ) + cg.add( + num_mult_var.set_initial_value( + sprinkler_controller[CONF_MULTIPLIER_NUMBER][CONF_INITIAL_VALUE] + ) + ) + cg.add( + num_mult_var.set_restore_value( + sprinkler_controller[CONF_MULTIPLIER_NUMBER][CONF_RESTORE_VALUE] + ) + ) + + if CONF_SET_ACTION in sprinkler_controller[CONF_MULTIPLIER_NUMBER]: + await automation.build_automation( + num_mult_var.get_set_trigger(), + [(float, "x")], + sprinkler_controller[CONF_MULTIPLIER_NUMBER][CONF_SET_ACTION], + ) + + cg.add(var.set_controller_multiplier_number(num_mult_var)) + + if CONF_REPEAT_NUMBER in sprinkler_controller: + num_repeat_var = await number.new_number( + sprinkler_controller[CONF_REPEAT_NUMBER], + min_value=sprinkler_controller[CONF_REPEAT_NUMBER][CONF_MIN_VALUE], + max_value=sprinkler_controller[CONF_REPEAT_NUMBER][CONF_MAX_VALUE], + step=sprinkler_controller[CONF_REPEAT_NUMBER][CONF_STEP], + ) + await cg.register_component( + num_repeat_var, sprinkler_controller[CONF_REPEAT_NUMBER] + ) + cg.add( + num_repeat_var.set_initial_value( + sprinkler_controller[CONF_REPEAT_NUMBER][CONF_INITIAL_VALUE] + ) + ) + cg.add( + num_repeat_var.set_restore_value( + sprinkler_controller[CONF_REPEAT_NUMBER][CONF_RESTORE_VALUE] + ) + ) + + if CONF_SET_ACTION in sprinkler_controller[CONF_REPEAT_NUMBER]: + await automation.build_automation( + num_repeat_var.get_set_trigger(), + [(float, "x")], + sprinkler_controller[CONF_REPEAT_NUMBER][CONF_SET_ACTION], + ) + + cg.add(var.set_controller_repeat_number(num_repeat_var)) + for valve in sprinkler_controller[CONF_VALVES]: sw_valve_var = await switch.new_switch(valve[CONF_VALVE_SWITCH]) await cg.register_component(sw_valve_var, valve[CONF_VALVE_SWITCH]) @@ -470,6 +690,12 @@ async def to_code(config): else: cg.add(var.add_valve(sw_valve_var)) + cg.add( + var.set_next_prev_ignore_disabled_valves( + sprinkler_controller[CONF_NEXT_PREV_IGNORE_DISABLED] + ) + ) + if CONF_MANUAL_SELECTION_DELAY in sprinkler_controller: cg.add( var.set_manual_selection_delay( @@ -524,6 +750,11 @@ async def to_code(config): for sprinkler_controller in config: var = await cg.get_variable(sprinkler_controller[CONF_ID]) for valve_index, valve in enumerate(sprinkler_controller[CONF_VALVES]): + if CONF_RUN_DURATION not in valve: + valve[CONF_RUN_DURATION] = valve[CONF_RUN_DURATION_NUMBER][ + CONF_INITIAL_VALUE + ] + if CONF_VALVE_SWITCH_ID in valve: valve_switch = await cg.get_variable(valve[CONF_VALVE_SWITCH_ID]) cg.add( @@ -561,6 +792,35 @@ async def to_code(config): ) ) + if CONF_RUN_DURATION_NUMBER in valve: + num_rd_var = await number.new_number( + valve[CONF_RUN_DURATION_NUMBER], + min_value=valve[CONF_RUN_DURATION_NUMBER][CONF_MIN_VALUE], + max_value=valve[CONF_RUN_DURATION_NUMBER][CONF_MAX_VALUE], + step=valve[CONF_RUN_DURATION_NUMBER][CONF_STEP], + ) + await cg.register_component(num_rd_var, valve[CONF_RUN_DURATION_NUMBER]) + + cg.add( + num_rd_var.set_initial_value( + valve[CONF_RUN_DURATION_NUMBER][CONF_INITIAL_VALUE] + ) + ) + cg.add( + num_rd_var.set_restore_value( + valve[CONF_RUN_DURATION_NUMBER][CONF_RESTORE_VALUE] + ) + ) + + if CONF_SET_ACTION in valve[CONF_RUN_DURATION_NUMBER]: + await automation.build_automation( + num_rd_var.get_set_trigger(), + [(float, "x")], + valve[CONF_RUN_DURATION_NUMBER][CONF_SET_ACTION], + ) + + cg.add(var.configure_valve_run_duration_number(valve_index, num_rd_var)) + for sprinkler_controller in config: var = await cg.get_variable(sprinkler_controller[CONF_ID]) for controller_to_add in config: diff --git a/esphome/components/sprinkler/automation.h b/esphome/components/sprinkler/automation.h index dd0ea44633..59c6cd50e1 100644 --- a/esphome/components/sprinkler/automation.h +++ b/esphome/components/sprinkler/automation.h @@ -7,6 +7,18 @@ namespace esphome { namespace sprinkler { +template class SetDividerAction : public Action { + public: + explicit SetDividerAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} + + TEMPLATABLE_VALUE(uint32_t, divider) + + void play(Ts... x) override { this->sprinkler_->set_divider(this->divider_.optional_value(x...)); } + + protected: + Sprinkler *sprinkler_; +}; + template class SetMultiplierAction : public Action { public: explicit SetMultiplierAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} @@ -98,8 +110,12 @@ template class StartSingleValveAction : public Action { explicit StartSingleValveAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} TEMPLATABLE_VALUE(size_t, valve_to_start) + TEMPLATABLE_VALUE(uint32_t, valve_run_duration) - void play(Ts... x) override { this->sprinkler_->start_single_valve(this->valve_to_start_.optional_value(x...)); } + void play(Ts... x) override { + this->sprinkler_->start_single_valve(this->valve_to_start_.optional_value(x...), + this->valve_run_duration_.optional_value(x...)); + } protected: Sprinkler *sprinkler_; diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp index 2be71a08d0..9d3044802d 100644 --- a/esphome/components/sprinkler/sprinkler.cpp +++ b/esphome/components/sprinkler/sprinkler.cpp @@ -75,6 +75,34 @@ void SprinklerSwitch::sync_valve_state(bool latch_state) { } } +void SprinklerControllerNumber::setup() { + float value; + if (!this->restore_value_) { + value = this->initial_value_; + } else { + this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); + if (!this->pref_.load(&value)) { + if (!std::isnan(this->initial_value_)) { + value = this->initial_value_; + } else { + value = this->traits.get_min_value(); + } + } + } + this->publish_state(value); +} + +void SprinklerControllerNumber::control(float value) { + this->set_trigger_->trigger(value); + + this->publish_state(value); + + if (this->restore_value_) + this->pref_.save(&value); +} + +void SprinklerControllerNumber::dump_config() { LOG_NUMBER("", "Sprinkler Controller Number", this); } + SprinklerControllerSwitch::SprinklerControllerSwitch() : turn_on_trigger_(new Trigger<>()), turn_off_trigger_(new Trigger<>()) {} @@ -101,12 +129,9 @@ void SprinklerControllerSwitch::write_state(bool state) { this->turn_off_trigger_->trigger(); } - if (this->optimistic_) - this->publish_state(state); + this->publish_state(state); } -void SprinklerControllerSwitch::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } -bool SprinklerControllerSwitch::assumed_state() { return this->assumed_state_; } void SprinklerControllerSwitch::set_state_lambda(std::function()> &&f) { this->f_ = f; } float SprinklerControllerSwitch::get_setup_priority() const { return setup_priority::HARDWARE; } @@ -114,30 +139,16 @@ Trigger<> *SprinklerControllerSwitch::get_turn_on_trigger() const { return this- Trigger<> *SprinklerControllerSwitch::get_turn_off_trigger() const { return this->turn_off_trigger_; } void SprinklerControllerSwitch::setup() { - if (!this->restore_state_) - return; + this->state = this->get_initial_state_with_restore_mode().value_or(false); - auto restored = this->get_initial_state(); - if (!restored.has_value()) - return; - - ESP_LOGD(TAG, " Restored state %s", ONOFF(*restored)); - if (*restored) { + if (this->state) { this->turn_on(); } else { this->turn_off(); } } -void SprinklerControllerSwitch::dump_config() { - LOG_SWITCH("", "Sprinkler Switch", this); - ESP_LOGCONFIG(TAG, " Restore State: %s", YESNO(this->restore_state_)); - ESP_LOGCONFIG(TAG, " Optimistic: %s", YESNO(this->optimistic_)); -} - -void SprinklerControllerSwitch::set_restore_state(bool restore_state) { this->restore_state_ = restore_state; } - -void SprinklerControllerSwitch::set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } +void SprinklerControllerSwitch::dump_config() { LOG_SWITCH("", "Sprinkler Switch", this); } SprinklerValveOperator::SprinklerValveOperator() {} SprinklerValveOperator::SprinklerValveOperator(SprinklerValve *valve, Sprinkler *controller) @@ -328,6 +339,8 @@ SprinklerValveRunRequest::SprinklerValveRunRequest(size_t valve_number, uint32_t bool SprinklerValveRunRequest::has_request() { return this->has_valve_; } bool SprinklerValveRunRequest::has_valve_operator() { return !(this->valve_op_ == nullptr); } +void SprinklerValveRunRequest::set_request_from(SprinklerValveRunRequestOrigin origin) { this->origin_ = origin; } + void SprinklerValveRunRequest::set_run_duration(uint32_t run_duration) { this->run_duration_ = run_duration; } void SprinklerValveRunRequest::set_valve(size_t valve_number) { @@ -345,6 +358,7 @@ void SprinklerValveRunRequest::set_valve_operator(SprinklerValveOperator *valve_ void SprinklerValveRunRequest::reset() { this->has_valve_ = false; + this->origin_ = USER; this->run_duration_ = 0; this->valve_op_ = nullptr; } @@ -362,6 +376,8 @@ optional SprinklerValveRunRequest::valve_as_opt() { SprinklerValveOperator *SprinklerValveRunRequest::valve_operator() { return this->valve_op_; } +SprinklerValveRunRequestOrigin SprinklerValveRunRequest::request_is_from() { return this->origin_; } + Sprinkler::Sprinkler() {} Sprinkler::Sprinkler(const std::string &name) : EntityBase(name) {} @@ -404,8 +420,6 @@ void Sprinkler::add_valve(SprinklerControllerSwitch *valve_sw, SprinklerControll if (enable_sw != nullptr) { new_valve->enable_switch = enable_sw; - new_valve->enable_switch->set_optimistic(true); - new_valve->enable_switch->set_restore_state(true); } } @@ -433,26 +447,37 @@ void Sprinkler::set_controller_main_switch(SprinklerControllerSwitch *controller void Sprinkler::set_controller_auto_adv_switch(SprinklerControllerSwitch *auto_adv_switch) { this->auto_adv_sw_ = auto_adv_switch; - auto_adv_switch->set_optimistic(true); - auto_adv_switch->set_restore_state(true); } void Sprinkler::set_controller_queue_enable_switch(SprinklerControllerSwitch *queue_enable_switch) { this->queue_enable_sw_ = queue_enable_switch; - queue_enable_switch->set_optimistic(true); - queue_enable_switch->set_restore_state(true); } void Sprinkler::set_controller_reverse_switch(SprinklerControllerSwitch *reverse_switch) { this->reverse_sw_ = reverse_switch; - reverse_switch->set_optimistic(true); - reverse_switch->set_restore_state(true); +} + +void Sprinkler::set_controller_standby_switch(SprinklerControllerSwitch *standby_switch) { + this->standby_sw_ = standby_switch; + + this->sprinkler_standby_turn_on_automation_ = make_unique>(standby_switch->get_turn_on_trigger()); + this->sprinkler_standby_shutdown_action_ = make_unique>(this); + this->sprinkler_standby_turn_on_automation_->add_actions({sprinkler_standby_shutdown_action_.get()}); +} + +void Sprinkler::set_controller_multiplier_number(SprinklerControllerNumber *multiplier_number) { + this->multiplier_number_ = multiplier_number; +} + +void Sprinkler::set_controller_repeat_number(SprinklerControllerNumber *repeat_number) { + this->repeat_number_ = repeat_number; } void Sprinkler::configure_valve_switch(size_t valve_number, switch_::Switch *valve_switch, uint32_t run_duration) { if (this->is_a_valid_valve(valve_number)) { this->valve_[valve_number].valve_switch.set_on_switch(valve_switch); this->valve_[valve_number].run_duration = run_duration; + valve_switch->turn_off(); } } @@ -464,6 +489,8 @@ void Sprinkler::configure_valve_switch_pulsed(size_t valve_number, switch_::Swit this->valve_[valve_number].valve_switch.set_on_switch(valve_switch_on); this->valve_[valve_number].valve_switch.set_pulse_duration(pulse_duration); this->valve_[valve_number].run_duration = run_duration; + valve_switch_off->turn_off(); + valve_switch_on->turn_off(); } } @@ -478,6 +505,7 @@ void Sprinkler::configure_valve_pump_switch(size_t valve_number, switch_::Switch this->pump_.resize(this->pump_.size() + 1); this->pump_.back().set_on_switch(pump_switch); this->valve_[valve_number].pump_switch_index = this->pump_.size() - 1; // save the index to the new pump + pump_switch->turn_off(); } } @@ -496,15 +524,49 @@ void Sprinkler::configure_valve_pump_switch_pulsed(size_t valve_number, switch_: this->pump_.back().set_on_switch(pump_switch_on); this->pump_.back().set_pulse_duration(pulse_duration); this->valve_[valve_number].pump_switch_index = this->pump_.size() - 1; // save the index to the new pump + pump_switch_off->turn_off(); + pump_switch_on->turn_off(); + } +} + +void Sprinkler::configure_valve_run_duration_number(size_t valve_number, + SprinklerControllerNumber *run_duration_number) { + if (this->is_a_valid_valve(valve_number)) { + this->valve_[valve_number].run_duration_number = run_duration_number; + } +} + +void Sprinkler::set_divider(optional divider) { + if (!divider.has_value()) { + return; + } + if (divider.value() > 0) { + this->set_multiplier(1.0 / divider.value()); + this->set_repeat(divider.value() - 1); + } else if (divider.value() == 0) { + this->set_multiplier(1.0); + this->set_repeat(0); } } void Sprinkler::set_multiplier(const optional multiplier) { - if (multiplier.has_value()) { - if (multiplier.value() > 0) { - this->multiplier_ = multiplier.value(); - } + if ((!multiplier.has_value()) || (multiplier.value() < 0)) { + return; } + this->multiplier_ = multiplier.value(); + if (this->multiplier_number_ == nullptr) { + return; + } + if (this->multiplier_number_->state == multiplier.value()) { + return; + } + auto call = this->multiplier_number_->make_call(); + call.set_value(multiplier.value()); + call.perform(); +} + +void Sprinkler::set_next_prev_ignore_disabled_valves(bool ignore_disabled) { + this->next_prev_ignore_disabled_ = ignore_disabled; } void Sprinkler::set_pump_start_delay(uint32_t start_delay) { @@ -559,47 +621,118 @@ void Sprinkler::set_manual_selection_delay(uint32_t manual_selection_delay) { } void Sprinkler::set_valve_run_duration(const optional valve_number, const optional run_duration) { - if (valve_number.has_value() && run_duration.has_value()) { - if (this->is_a_valid_valve(valve_number.value())) { - this->valve_[valve_number.value()].run_duration = run_duration.value(); - } + if (!valve_number.has_value() || !run_duration.has_value()) { + return; } + if (!this->is_a_valid_valve(valve_number.value())) { + return; + } + this->valve_[valve_number.value()].run_duration = run_duration.value(); + if (this->valve_[valve_number.value()].run_duration_number == nullptr) { + return; + } + if (this->valve_[valve_number.value()].run_duration_number->state == run_duration.value()) { + return; + } + auto call = this->valve_[valve_number.value()].run_duration_number->make_call(); + if (this->valve_[valve_number.value()].run_duration_number->traits.get_unit_of_measurement() == min_str) { + call.set_value(run_duration.value() / 60.0); + } else { + call.set_value(run_duration.value()); + } + call.perform(); } void Sprinkler::set_auto_advance(const bool auto_advance) { - if (this->auto_adv_sw_ != nullptr) { - this->auto_adv_sw_->publish_state(auto_advance); + if (this->auto_adv_sw_ == nullptr) { + return; + } + if (this->auto_adv_sw_->state == auto_advance) { + return; + } + if (auto_advance) { + this->auto_adv_sw_->turn_on(); + } else { + this->auto_adv_sw_->turn_off(); } } -void Sprinkler::set_repeat(optional repeat) { this->target_repeats_ = repeat; } +void Sprinkler::set_repeat(optional repeat) { + this->target_repeats_ = repeat; + if (this->repeat_number_ == nullptr) { + return; + } + if (this->repeat_number_->state == repeat.value()) { + return; + } + auto call = this->repeat_number_->make_call(); + call.set_value(repeat.value_or(0)); + call.perform(); +} void Sprinkler::set_queue_enable(bool queue_enable) { - if (this->queue_enable_sw_ != nullptr) { - this->queue_enable_sw_->publish_state(queue_enable); + if (this->queue_enable_sw_ == nullptr) { + return; + } + if (this->queue_enable_sw_->state == queue_enable) { + return; + } + if (queue_enable) { + this->queue_enable_sw_->turn_on(); + } else { + this->queue_enable_sw_->turn_off(); } } void Sprinkler::set_reverse(const bool reverse) { - if (this->reverse_sw_ != nullptr) { - this->reverse_sw_->publish_state(reverse); + if (this->reverse_sw_ == nullptr) { + return; + } + if (this->reverse_sw_->state == reverse) { + return; + } + if (reverse) { + this->reverse_sw_->turn_on(); + } else { + this->reverse_sw_->turn_off(); + } +} + +void Sprinkler::set_standby(const bool standby) { + if (this->standby_sw_ == nullptr) { + return; + } + if (this->standby_sw_->state == standby) { + return; + } + if (standby) { + this->standby_sw_->turn_on(); + } else { + this->standby_sw_->turn_off(); } } uint32_t Sprinkler::valve_run_duration(const size_t valve_number) { - if (this->is_a_valid_valve(valve_number)) { - return this->valve_[valve_number].run_duration; + if (!this->is_a_valid_valve(valve_number)) { + return 0; } - return 0; + if (this->valve_[valve_number].run_duration_number != nullptr) { + if (this->valve_[valve_number].run_duration_number->traits.get_unit_of_measurement() == min_str) { + return static_cast(roundf(this->valve_[valve_number].run_duration_number->state * 60)); + } else { + return static_cast(roundf(this->valve_[valve_number].run_duration_number->state)); + } + } + return this->valve_[valve_number].run_duration; } uint32_t Sprinkler::valve_run_duration_adjusted(const size_t valve_number) { uint32_t run_duration = 0; if (this->is_a_valid_valve(valve_number)) { - run_duration = this->valve_[valve_number].run_duration; + run_duration = this->valve_run_duration(valve_number); } - run_duration = static_cast(roundf(run_duration * this->multiplier_)); + run_duration = static_cast(roundf(run_duration * this->multiplier())); // run_duration must not be less than any of these if ((run_duration < this->start_delay_) || (run_duration < this->stop_delay_) || (run_duration < this->switching_delay_.value_or(0) * 2)) { @@ -615,16 +748,24 @@ bool Sprinkler::auto_advance() { return false; } -float Sprinkler::multiplier() { return this->multiplier_; } +float Sprinkler::multiplier() { + if (this->multiplier_number_ != nullptr) { + return this->multiplier_number_->state; + } + return this->multiplier_; +} -optional Sprinkler::repeat() { return this->target_repeats_; } +optional Sprinkler::repeat() { + if (this->repeat_number_ != nullptr) { + return static_cast(roundf(this->repeat_number_->state)); + } + return this->target_repeats_; +} optional Sprinkler::repeat_count() { // if there is an active valve and auto-advance is enabled, we may be repeating, so return the count - if (this->auto_adv_sw_ != nullptr) { - if (this->active_req_.has_request() && this->auto_adv_sw_->state) { - return this->repeat_count_; - } + if (this->active_req_.has_request() && this->auto_advance()) { + return this->repeat_count_; } return nullopt; } @@ -643,7 +784,22 @@ bool Sprinkler::reverse() { return false; } +bool Sprinkler::standby() { + if (this->standby_sw_ != nullptr) { + return this->standby_sw_->state; + } + return false; +} + void Sprinkler::start_from_queue() { + if (this->standby()) { + ESP_LOGD(TAG, "start_from_queue called but standby is enabled; no action taken"); + return; + } + if (this->multiplier() == 0) { + ESP_LOGD(TAG, "start_from_queue called but multiplier is set to zero; no action taken"); + return; + } if (this->queued_valves_.empty()) { return; // if there is nothing in the queue, don't do anything } @@ -651,25 +807,29 @@ void Sprinkler::start_from_queue() { return; // if there is already a valve running from the queue, do nothing } - if (this->auto_adv_sw_ != nullptr) { - this->auto_adv_sw_->publish_state(false); - } - if (this->queue_enable_sw_ != nullptr) { - this->queue_enable_sw_->publish_state(true); - } + this->set_auto_advance(false); + this->set_queue_enable(true); + this->reset_cycle_states_(); // just in case auto-advance is switched on later this->repeat_count_ = 0; this->fsm_kick_(); // will automagically pick up from the queue (it has priority) } void Sprinkler::start_full_cycle() { + if (this->standby()) { + ESP_LOGD(TAG, "start_full_cycle called but standby is enabled; no action taken"); + return; + } + if (this->multiplier() == 0) { + ESP_LOGD(TAG, "start_full_cycle called but multiplier is set to zero; no action taken"); + return; + } if (this->auto_advance() && this->active_valve().has_value()) { return; // if auto-advance is already enabled and there is already a valve running, do nothing } - if (this->queue_enable_sw_ != nullptr) { - this->queue_enable_sw_->publish_state(false); - } + this->set_queue_enable(false); + this->prep_full_cycle_(); this->repeat_count_ = 0; // if there is no active valve already, start the first valve in the cycle @@ -678,20 +838,25 @@ void Sprinkler::start_full_cycle() { } } -void Sprinkler::start_single_valve(const optional valve_number) { +void Sprinkler::start_single_valve(const optional valve_number, optional run_duration) { + if (this->standby()) { + ESP_LOGD(TAG, "start_single_valve called but standby is enabled; no action taken"); + return; + } + if (this->multiplier() == 0) { + ESP_LOGD(TAG, "start_single_valve called but multiplier is set to zero; no action taken"); + return; + } if (!valve_number.has_value() || (valve_number == this->active_valve())) { return; } - if (this->auto_adv_sw_ != nullptr) { - this->auto_adv_sw_->publish_state(false); - } - if (this->queue_enable_sw_ != nullptr) { - this->queue_enable_sw_->publish_state(false); - } + this->set_auto_advance(false); + this->set_queue_enable(false); + this->reset_cycle_states_(); // just in case auto-advance is switched on later this->repeat_count_ = 0; - this->fsm_request_(valve_number.value()); + this->fsm_request_(valve_number.value(), run_duration.value_or(0)); } void Sprinkler::queue_valve(optional valve_number, optional run_duration) { @@ -714,8 +879,17 @@ void Sprinkler::next_valve() { if (this->state_ == IDLE) { this->reset_cycle_states_(); // just in case auto-advance is switched on later } + this->manual_valve_ = this->next_valve_number_( - this->manual_valve_.value_or(this->active_req_.valve_as_opt().value_or(this->number_of_valves() - 1))); + this->manual_valve_.value_or(this->active_req_.valve_as_opt().value_or(this->number_of_valves() - 1)), + !this->next_prev_ignore_disabled_, true); + + if (!this->manual_valve_.has_value()) { + ESP_LOGD(TAG, "next_valve was called but no valve could be started; perhaps next_prev_ignore_disabled allows only " + "enabled valves and no valves are enabled?"); + return; + } + if (this->manual_selection_delay_.has_value()) { this->set_timer_duration_(sprinkler::TIMER_VALVE_SELECTION, this->manual_selection_delay_.value()); this->start_timer_(sprinkler::TIMER_VALVE_SELECTION); @@ -728,8 +902,17 @@ void Sprinkler::previous_valve() { if (this->state_ == IDLE) { this->reset_cycle_states_(); // just in case auto-advance is switched on later } + this->manual_valve_ = - this->previous_valve_number_(this->manual_valve_.value_or(this->active_req_.valve_as_opt().value_or(0))); + this->previous_valve_number_(this->manual_valve_.value_or(this->active_req_.valve_as_opt().value_or(0)), + !this->next_prev_ignore_disabled_, true); + + if (!this->manual_valve_.has_value()) { + ESP_LOGD(TAG, "previous_valve was called but no valve could be started; perhaps next_prev_ignore_disabled allows " + "only enabled valves and no valves are enabled?"); + return; + } + if (this->manual_selection_delay_.has_value()) { this->set_timer_duration_(sprinkler::TIMER_VALVE_SELECTION, this->manual_selection_delay_.value()); this->start_timer_(sprinkler::TIMER_VALVE_SELECTION); @@ -758,7 +941,7 @@ void Sprinkler::pause() { return; // we can't pause if we're already paused or if there is no active valve } this->paused_valve_ = this->active_valve(); - this->resume_duration_ = this->time_remaining(); + this->resume_duration_ = this->time_remaining_active_valve(); this->shutdown(false); ESP_LOGD(TAG, "Paused valve %u with %u seconds remaining", this->paused_valve_.value_or(0), this->resume_duration_.value_or(0)); @@ -795,6 +978,13 @@ const char *Sprinkler::valve_name(const size_t valve_number) { return nullptr; } +optional Sprinkler::active_valve_request_is_from() { + if (this->active_req_.has_request()) { + return this->active_req_.request_is_from(); + } + return nullopt; +} + optional Sprinkler::active_valve() { return this->active_req_.valve_as_opt(); } optional Sprinkler::paused_valve() { return this->paused_valve_; } @@ -829,8 +1019,7 @@ bool Sprinkler::pump_in_use(SprinklerSwitch *pump_switch) { if ((vo.pump_switch()->off_switch() == pump_switch->off_switch()) && (vo.pump_switch()->on_switch() == pump_switch->on_switch())) { // now if the SprinklerValveOperator has a pump and it is either ACTIVE, is STARTING with a valve delay or - // is - // STOPPING with a valve delay, its pump can be considered "in use", so just return indicating this now + // is STOPPING with a valve delay, its pump can be considered "in use", so just return indicating this now if ((vo.state() == ACTIVE) || ((vo.state() == STARTING) && this->start_delay_ && this->start_delay_is_valve_delay_) || ((vo.state() == STOPPING) && this->stop_delay_ && this->stop_delay_is_valve_delay_)) { @@ -881,7 +1070,93 @@ void Sprinkler::set_pump_state(SprinklerSwitch *pump_switch, bool state) { } } -optional Sprinkler::time_remaining() { +uint32_t Sprinkler::total_cycle_time_all_valves() { + uint32_t total_time_remaining = 0; + + for (size_t valve = 0; valve < this->number_of_valves(); valve++) { + total_time_remaining += this->valve_run_duration_adjusted(valve); + } + + if (this->valve_overlap_) { + total_time_remaining -= this->switching_delay_.value_or(0) * (this->number_of_valves() - 1); + } else { + total_time_remaining += this->switching_delay_.value_or(0) * (this->number_of_valves() - 1); + } + + return total_time_remaining; +} + +uint32_t Sprinkler::total_cycle_time_enabled_valves() { + uint32_t total_time_remaining = 0; + uint32_t valve_count = 0; + + for (size_t valve = 0; valve < this->number_of_valves(); valve++) { + if (this->valve_is_enabled_(valve)) { + total_time_remaining += this->valve_run_duration_adjusted(valve); + valve_count++; + } + } + + if (valve_count) { + if (this->valve_overlap_) { + total_time_remaining -= this->switching_delay_.value_or(0) * (valve_count - 1); + } else { + total_time_remaining += this->switching_delay_.value_or(0) * (valve_count - 1); + } + } + + return total_time_remaining; +} + +uint32_t Sprinkler::total_cycle_time_enabled_incomplete_valves() { + uint32_t total_time_remaining = 0; + uint32_t valve_count = 0; + + for (size_t valve = 0; valve < this->number_of_valves(); valve++) { + if (this->valve_is_enabled_(valve) && !this->valve_cycle_complete_(valve)) { + if (!this->active_valve().has_value() || (valve != this->active_valve().value())) { + total_time_remaining += this->valve_run_duration_adjusted(valve); + valve_count++; + } + } + } + + if (valve_count) { + if (this->valve_overlap_) { + total_time_remaining -= this->switching_delay_.value_or(0) * (valve_count - 1); + } else { + total_time_remaining += this->switching_delay_.value_or(0) * (valve_count - 1); + } + } + + return total_time_remaining; +} + +uint32_t Sprinkler::total_queue_time() { + uint32_t total_time_remaining = 0; + uint32_t valve_count = 0; + + for (auto &valve : this->queued_valves_) { + if (valve.run_duration) { + total_time_remaining += valve.run_duration; + } else { + total_time_remaining += this->valve_run_duration_adjusted(valve.valve_number); + } + valve_count++; + } + + if (valve_count) { + if (this->valve_overlap_) { + total_time_remaining -= this->switching_delay_.value_or(0) * (valve_count - 1); + } else { + total_time_remaining += this->switching_delay_.value_or(0) * (valve_count - 1); + } + } + + return total_time_remaining; +} + +optional Sprinkler::time_remaining_active_valve() { if (this->active_req_.has_request()) { // first try to return the value based on active_req_... if (this->active_req_.valve_operator() != nullptr) { return this->active_req_.valve_operator()->time_remaining(); @@ -895,6 +1170,25 @@ optional Sprinkler::time_remaining() { return nullopt; } +optional Sprinkler::time_remaining_current_operation() { + auto total_time_remaining = this->time_remaining_active_valve(); + + if (total_time_remaining.has_value()) { + if (this->auto_advance()) { + total_time_remaining = total_time_remaining.value() + this->total_cycle_time_enabled_incomplete_valves(); + total_time_remaining = + total_time_remaining.value() + + (this->total_cycle_time_enabled_valves() * (this->repeat().value_or(0) - this->repeat_count().value_or(0))); + } + + if (this->queue_enabled()) { + total_time_remaining = total_time_remaining.value() + this->total_queue_time(); + } + return total_time_remaining; + } + return nullopt; +} + SprinklerControllerSwitch *Sprinkler::control_switch(size_t valve_number) { if (this->is_a_valid_valve(valve_number)) { return this->valve_[valve_number].controller_switch; @@ -957,30 +1251,60 @@ bool Sprinkler::valve_cycle_complete_(const size_t valve_number) { return false; } -size_t Sprinkler::next_valve_number_(const size_t first_valve) { - if (this->is_a_valid_valve(first_valve) && (first_valve + 1 < this->number_of_valves())) - return first_valve + 1; +optional Sprinkler::next_valve_number_(const optional first_valve, const bool include_disabled, + const bool include_complete) { + auto valve = first_valve.value_or(0); + size_t start = first_valve.has_value() ? 1 : 0; - return 0; + if (!this->is_a_valid_valve(valve)) { + valve = 0; + } + + for (size_t offset = start; offset < this->number_of_valves(); offset++) { + auto valve_of_interest = valve + offset; + if (!this->is_a_valid_valve(valve_of_interest)) { + valve_of_interest -= this->number_of_valves(); + } + + if ((this->valve_is_enabled_(valve_of_interest) || include_disabled) && + (!this->valve_cycle_complete_(valve_of_interest) || include_complete)) { + return valve_of_interest; + } + } + return nullopt; } -size_t Sprinkler::previous_valve_number_(const size_t first_valve) { - if (this->is_a_valid_valve(first_valve) && (first_valve - 1 >= 0)) - return first_valve - 1; +optional Sprinkler::previous_valve_number_(const optional first_valve, const bool include_disabled, + const bool include_complete) { + auto valve = first_valve.value_or(this->number_of_valves() - 1); + size_t start = first_valve.has_value() ? 1 : 0; - return this->number_of_valves() - 1; + if (!this->is_a_valid_valve(valve)) { + valve = this->number_of_valves() - 1; + } + + for (size_t offset = start; offset < this->number_of_valves(); offset++) { + auto valve_of_interest = valve - offset; + if (!this->is_a_valid_valve(valve_of_interest)) { + valve_of_interest += this->number_of_valves(); + } + + if ((this->valve_is_enabled_(valve_of_interest) || include_disabled) && + (!this->valve_cycle_complete_(valve_of_interest) || include_complete)) { + return valve_of_interest; + } + } + return nullopt; } optional Sprinkler::next_valve_number_in_cycle_(const optional first_valve) { - if (this->reverse_sw_ != nullptr) { - if (this->reverse_sw_->state) { - return this->previous_enabled_incomplete_valve_number_(first_valve); - } + if (this->reverse()) { + return this->previous_valve_number_(first_valve, false, false); } - return this->next_enabled_incomplete_valve_number_(first_valve); + return this->next_valve_number_(first_valve, false, false); } -void Sprinkler::load_next_valve_run_request_(optional first_valve) { +void Sprinkler::load_next_valve_run_request_(const optional first_valve) { if (this->next_req_.has_request()) { if (!this->next_req_.run_duration()) { // ensure the run duration is set correctly for consumption later on this->next_req_.set_run_duration(this->valve_run_duration_adjusted(this->next_req_.valve())); @@ -988,58 +1312,37 @@ void Sprinkler::load_next_valve_run_request_(optional first_valve) { return; // there is already a request pending } else if (this->queue_enabled() && !this->queued_valves_.empty()) { this->next_req_.set_valve(this->queued_valves_.back().valve_number); + this->next_req_.set_request_from(QUEUE); if (this->queued_valves_.back().run_duration) { this->next_req_.set_run_duration(this->queued_valves_.back().run_duration); - } else { + this->queued_valves_.pop_back(); + } else if (this->multiplier()) { this->next_req_.set_run_duration(this->valve_run_duration_adjusted(this->queued_valves_.back().valve_number)); + this->queued_valves_.pop_back(); + } else { + this->next_req_.reset(); } - this->queued_valves_.pop_back(); - } else if (this->auto_adv_sw_ != nullptr) { - if (this->auto_adv_sw_->state) { - if (this->next_valve_number_in_cycle_(first_valve).has_value()) { - // if there is another valve to run as a part of a cycle, load that - this->next_req_.set_valve(this->next_valve_number_in_cycle_(first_valve).value_or(0)); + } else if (this->auto_advance() && this->multiplier()) { + if (this->next_valve_number_in_cycle_(first_valve).has_value()) { + // if there is another valve to run as a part of a cycle, load that + this->next_req_.set_valve(this->next_valve_number_in_cycle_(first_valve).value_or(0)); + this->next_req_.set_request_from(CYCLE); + this->next_req_.set_run_duration( + this->valve_run_duration_adjusted(this->next_valve_number_in_cycle_(first_valve).value_or(0))); + } else if ((this->repeat_count_++ < this->repeat().value_or(0))) { + ESP_LOGD(TAG, "Repeating - starting cycle %u of %u", this->repeat_count_ + 1, this->repeat().value_or(0) + 1); + // if there are repeats remaining and no more valves were left in the cycle, start a new cycle + this->prep_full_cycle_(); + if (this->next_valve_number_in_cycle_().has_value()) { // this should always succeed here, but just in case... + this->next_req_.set_valve(this->next_valve_number_in_cycle_().value_or(0)); + this->next_req_.set_request_from(CYCLE); this->next_req_.set_run_duration( - this->valve_run_duration_adjusted(this->next_valve_number_in_cycle_(first_valve).value_or(0))); - } else if ((this->repeat_count_++ < this->target_repeats_.value_or(0))) { - ESP_LOGD(TAG, "Repeating - starting cycle %u of %u", this->repeat_count_ + 1, - this->target_repeats_.value_or(0) + 1); - // if there are repeats remaining and no more valves were left in the cycle, start a new cycle - this->prep_full_cycle_(); - this->next_req_.set_valve(this->next_valve_number_in_cycle_(first_valve).value_or(0)); - this->next_req_.set_run_duration( - this->valve_run_duration_adjusted(this->next_valve_number_in_cycle_(first_valve).value_or(0))); + this->valve_run_duration_adjusted(this->next_valve_number_in_cycle_().value_or(0))); } } } } -optional Sprinkler::next_enabled_incomplete_valve_number_(const optional first_valve) { - auto new_valve_number = this->next_valve_number_(first_valve.value_or(this->number_of_valves() - 1)); - - while (new_valve_number != first_valve.value_or(this->number_of_valves() - 1)) { - if (this->valve_is_enabled_(new_valve_number) && (!this->valve_cycle_complete_(new_valve_number))) { - return new_valve_number; - } else { - new_valve_number = this->next_valve_number_(new_valve_number); - } - } - return nullopt; -} - -optional Sprinkler::previous_enabled_incomplete_valve_number_(const optional first_valve) { - auto new_valve_number = this->previous_valve_number_(first_valve.value_or(0)); - - while (new_valve_number != first_valve.value_or(0)) { - if (this->valve_is_enabled_(new_valve_number) && (!this->valve_cycle_complete_(new_valve_number))) { - return new_valve_number; - } else { - new_valve_number = this->previous_valve_number_(new_valve_number); - } - } - return nullopt; -} - bool Sprinkler::any_valve_is_enabled_() { for (size_t valve_number = 0; valve_number < this->number_of_valves(); valve_number++) { if (this->valve_is_enabled_(valve_number)) @@ -1058,8 +1361,9 @@ void Sprinkler::start_valve_(SprinklerValveRunRequest *req) { for (auto &vo : this->valve_op_) { // find the first available SprinklerValveOperator, load it and start it up if (vo.state() == IDLE) { auto run_duration = req->run_duration() ? req->run_duration() : this->valve_run_duration_adjusted(req->valve()); - ESP_LOGD(TAG, "Starting valve %u for %u seconds, cycle %u of %u", req->valve(), run_duration, - this->repeat_count_ + 1, this->target_repeats_.value_or(0) + 1); + ESP_LOGD(TAG, "%s is starting valve %u for %u seconds, cycle %u of %u", + this->req_as_str_(req->request_is_from()).c_str(), req->valve(), run_duration, this->repeat_count_ + 1, + this->repeat().value_or(0) + 1); req->set_valve_operator(&vo); vo.set_controller(this); vo.set_valve(&this->valve_[req->valve()]); @@ -1085,15 +1389,14 @@ void Sprinkler::all_valves_off_(const bool include_pump) { } void Sprinkler::prep_full_cycle_() { - if (this->auto_adv_sw_ != nullptr) { - if (!this->auto_adv_sw_->state) { - this->auto_adv_sw_->publish_state(true); - } - } + this->set_auto_advance(true); + if (!this->any_valve_is_enabled_()) { for (auto &valve : this->valve_) { if (valve.enable_switch != nullptr) { - valve.enable_switch->publish_state(true); + if (!valve.enable_switch->state) { + valve.enable_switch->turn_on(); + } } } } @@ -1169,14 +1472,19 @@ void Sprinkler::fsm_transition_() { void Sprinkler::fsm_transition_from_shutdown_() { this->load_next_valve_run_request_(); - this->active_req_.set_valve(this->next_req_.valve()); - this->active_req_.set_run_duration(this->next_req_.run_duration()); - this->next_req_.reset(); - this->set_timer_duration_(sprinkler::TIMER_SM, this->active_req_.run_duration() - this->switching_delay_.value_or(0)); - this->start_timer_(sprinkler::TIMER_SM); - this->start_valve_(&this->active_req_); - this->state_ = ACTIVE; + if (this->next_req_.has_request()) { // there is a valve to run... + this->active_req_.set_valve(this->next_req_.valve()); + this->active_req_.set_request_from(this->next_req_.request_is_from()); + this->active_req_.set_run_duration(this->next_req_.run_duration()); + this->next_req_.reset(); + + this->set_timer_duration_(sprinkler::TIMER_SM, + this->active_req_.run_duration() - this->switching_delay_.value_or(0)); + this->start_timer_(sprinkler::TIMER_SM); + this->start_valve_(&this->active_req_); + this->state_ = ACTIVE; + } } void Sprinkler::fsm_transition_from_valve_run_() { @@ -1186,7 +1494,9 @@ void Sprinkler::fsm_transition_from_valve_run_() { } if (!this->timer_active_(sprinkler::TIMER_SM)) { // only flag the valve as "complete" if the timer finished - this->mark_valve_cycle_complete_(this->active_req_.valve()); + if ((this->active_req_.request_is_from() == CYCLE) || (this->active_req_.request_is_from() == USER)) { + this->mark_valve_cycle_complete_(this->active_req_.valve()); + } } else { ESP_LOGD(TAG, "Valve cycle interrupted - NOT flagging valve as complete and stopping current valve"); for (auto &vo : this->valve_op_) { @@ -1201,6 +1511,7 @@ void Sprinkler::fsm_transition_from_valve_run_() { this->valve_pump_switch(this->active_req_.valve()) == this->valve_pump_switch(this->next_req_.valve()); this->active_req_.set_valve(this->next_req_.valve()); + this->active_req_.set_request_from(this->next_req_.request_is_from()); this->active_req_.set_run_duration(this->next_req_.run_duration()); this->next_req_.reset(); @@ -1230,6 +1541,22 @@ void Sprinkler::fsm_transition_to_shutdown_() { this->start_timer_(sprinkler::TIMER_SM); } +std::string Sprinkler::req_as_str_(SprinklerValveRunRequestOrigin origin) { + switch (origin) { + case USER: + return "USER"; + + case CYCLE: + return "CYCLE"; + + case QUEUE: + return "QUEUE"; + + default: + return "UNKNOWN"; + } +} + std::string Sprinkler::state_as_str_(SprinklerState state) { switch (state) { case IDLE: @@ -1300,8 +1627,8 @@ void Sprinkler::dump_config() { if (this->manual_selection_delay_.has_value()) { ESP_LOGCONFIG(TAG, " Manual Selection Delay: %u seconds", this->manual_selection_delay_.value_or(0)); } - if (this->target_repeats_.has_value()) { - ESP_LOGCONFIG(TAG, " Repeat Cycles: %u times", this->target_repeats_.value_or(0)); + if (this->repeat().has_value()) { + ESP_LOGCONFIG(TAG, " Repeat Cycles: %u times", this->repeat().value_or(0)); } if (this->start_delay_) { if (this->start_delay_is_valve_delay_) { @@ -1329,7 +1656,7 @@ void Sprinkler::dump_config() { for (size_t valve_number = 0; valve_number < this->number_of_valves(); valve_number++) { ESP_LOGCONFIG(TAG, " Valve %u:", valve_number); ESP_LOGCONFIG(TAG, " Name: %s", this->valve_name(valve_number)); - ESP_LOGCONFIG(TAG, " Run Duration: %u seconds", this->valve_[valve_number].run_duration); + ESP_LOGCONFIG(TAG, " Run Duration: %u seconds", this->valve_run_duration(valve_number)); if (this->valve_[valve_number].valve_switch.pulse_duration()) { ESP_LOGCONFIG(TAG, " Pulse Duration: %u milliseconds", this->valve_[valve_number].valve_switch.pulse_duration()); diff --git a/esphome/components/sprinkler/sprinkler.h b/esphome/components/sprinkler/sprinkler.h index 625118d9e5..1b8c7e4528 100644 --- a/esphome/components/sprinkler/sprinkler.h +++ b/esphome/components/sprinkler/sprinkler.h @@ -3,6 +3,7 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/hal.h" +#include "esphome/components/number/number.h" #include "esphome/components/switch/switch.h" #include @@ -10,6 +11,8 @@ namespace esphome { namespace sprinkler { +const std::string min_str = "min"; + enum SprinklerState : uint8_t { // NOTE: these states are used by both SprinklerValveOperator and Sprinkler (the controller)! IDLE, // system/valve is off @@ -24,7 +27,14 @@ enum SprinklerTimerIndex : uint8_t { TIMER_VALVE_SELECTION = 1, }; +enum SprinklerValveRunRequestOrigin : uint8_t { + USER, + CYCLE, + QUEUE, +}; + class Sprinkler; // this component +class SprinklerControllerNumber; // number components that appear in the front end; based on number core class SprinklerControllerSwitch; // switches that appear in the front end; based on switch core class SprinklerSwitch; // switches representing any valve or pump; provides abstraction for latching valves class SprinklerValveOperator; // manages all switching on/off of valves and associated pumps @@ -76,6 +86,7 @@ struct SprinklerTimer { }; struct SprinklerValve { + SprinklerControllerNumber *run_duration_number; SprinklerControllerSwitch *controller_switch; SprinklerControllerSwitch *enable_switch; SprinklerSwitch valve_switch; @@ -88,6 +99,25 @@ struct SprinklerValve { std::unique_ptr> valve_turn_on_automation; }; +class SprinklerControllerNumber : public number::Number, public Component { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + Trigger *get_set_trigger() const { return set_trigger_; } + void set_initial_value(float initial_value) { initial_value_ = initial_value; } + void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; } + + protected: + void control(float value) override; + float initial_value_{NAN}; + bool restore_value_{true}; + Trigger *set_trigger_ = new Trigger(); + + ESPPreferenceObject pref_; +}; + class SprinklerControllerSwitch : public switch_::Switch, public Component { public: SprinklerControllerSwitch(); @@ -96,27 +126,19 @@ class SprinklerControllerSwitch : public switch_::Switch, public Component { void dump_config() override; void set_state_lambda(std::function()> &&f); - void set_restore_state(bool restore_state); Trigger<> *get_turn_on_trigger() const; Trigger<> *get_turn_off_trigger() const; - void set_optimistic(bool optimistic); - void set_assumed_state(bool assumed_state); void loop() override; float get_setup_priority() const override; protected: - bool assumed_state() override; - void write_state(bool state) override; optional()>> f_; - bool optimistic_{false}; - bool assumed_state_{false}; Trigger<> *turn_on_trigger_; Trigger<> *turn_off_trigger_; Trigger<> *prev_trigger_{nullptr}; - bool restore_state_{false}; }; class SprinklerValveOperator { @@ -160,6 +182,7 @@ class SprinklerValveRunRequest { SprinklerValveRunRequest(size_t valve_number, uint32_t run_duration, SprinklerValveOperator *valve_op); bool has_request(); bool has_valve_operator(); + void set_request_from(SprinklerValveRunRequestOrigin origin); void set_run_duration(uint32_t run_duration); void set_valve(size_t valve_number); void set_valve_operator(SprinklerValveOperator *valve_op); @@ -168,12 +191,14 @@ class SprinklerValveRunRequest { size_t valve(); optional valve_as_opt(); SprinklerValveOperator *valve_operator(); + SprinklerValveRunRequestOrigin request_is_from(); protected: bool has_valve_{false}; size_t valve_number_{0}; uint32_t run_duration_{0}; SprinklerValveOperator *valve_op_{nullptr}; + SprinklerValveRunRequestOrigin origin_{USER}; }; class Sprinkler : public Component, public EntityBase { @@ -196,6 +221,11 @@ class Sprinkler : public Component, public EntityBase { void set_controller_auto_adv_switch(SprinklerControllerSwitch *auto_adv_switch); void set_controller_queue_enable_switch(SprinklerControllerSwitch *queue_enable_switch); void set_controller_reverse_switch(SprinklerControllerSwitch *reverse_switch); + void set_controller_standby_switch(SprinklerControllerSwitch *standby_switch); + + /// configure important controller number components + void set_controller_multiplier_number(SprinklerControllerNumber *multiplier_number); + void set_controller_repeat_number(SprinklerControllerNumber *repeat_number); /// configure a valve's switch object and run duration. run_duration is time in seconds. void configure_valve_switch(size_t valve_number, switch_::Switch *valve_switch, uint32_t run_duration); @@ -207,9 +237,18 @@ class Sprinkler : public Component, public EntityBase { void configure_valve_pump_switch_pulsed(size_t valve_number, switch_::Switch *pump_switch_off, switch_::Switch *pump_switch_on, uint32_t pulse_duration); + /// configure a valve's run duration number component + void configure_valve_run_duration_number(size_t valve_number, SprinklerControllerNumber *run_duration_number); + + /// sets the multiplier value to '1 / divider' and sets repeat value to divider + void set_divider(optional divider); + /// value multiplied by configured run times -- used to extend or shorten the cycle void set_multiplier(optional multiplier); + /// enable/disable skipping of disabled valves by the next and previous actions + void set_next_prev_ignore_disabled_valves(bool ignore_disabled); + /// set how long the pump should start after the valve (when the pump is starting) void set_pump_start_delay(uint32_t start_delay); @@ -250,6 +289,9 @@ class Sprinkler : public Component, public EntityBase { /// if reverse is true, controller will iterate through all enabled valves in reverse (descending) order void set_reverse(bool reverse); + /// if standby is true, controller will refuse to activate any valves + void set_standby(bool standby); + /// returns valve_number's run duration in seconds uint32_t valve_run_duration(size_t valve_number); @@ -274,6 +316,9 @@ class Sprinkler : public Component, public EntityBase { /// returns true if reverse is enabled bool reverse(); + /// returns true if standby is enabled + bool standby(); + /// starts the controller from the first valve in the queue and disables auto_advance. /// if the queue is empty, does nothing. void start_from_queue(); @@ -283,7 +328,7 @@ class Sprinkler : public Component, public EntityBase { void start_full_cycle(); /// activates a single valve and disables auto_advance. - void start_single_valve(optional valve_number); + void start_single_valve(optional valve_number, optional run_duration = nullopt); /// adds a valve into the queue. queued valves have priority over valves to be run as a part of a full cycle. /// NOTE: queued valves will always run, regardless of auto-advance and/or valve enable switches. @@ -316,6 +361,9 @@ class Sprinkler : public Component, public EntityBase { /// returns a pointer to a valve's name string object; returns nullptr if valve_number is invalid const char *valve_name(size_t valve_number); + /// returns what invoked the valve that is currently active, if any. check with 'has_value()' + optional active_valve_request_is_from(); + /// returns the number of the valve that is currently active, if any. check with 'has_value()' optional active_valve(); @@ -341,8 +389,23 @@ class Sprinkler : public Component, public EntityBase { /// switches on/off a pump "safely" by checking that the new state will not conflict with another controller void set_pump_state(SprinklerSwitch *pump_switch, bool state); - /// returns the amount of time remaining in seconds for the active valve, if any. check with 'has_value()' - optional time_remaining(); + /// returns the amount of time in seconds required for all valves + uint32_t total_cycle_time_all_valves(); + + /// returns the amount of time in seconds required for all enabled valves + uint32_t total_cycle_time_enabled_valves(); + + /// returns the amount of time in seconds required for all enabled & incomplete valves, not including the active valve + uint32_t total_cycle_time_enabled_incomplete_valves(); + + /// returns the amount of time in seconds required for all valves in the queue + uint32_t total_queue_time(); + + /// returns the amount of time remaining in seconds for the active valve, if any + optional time_remaining_active_valve(); + + /// returns the amount of time remaining in seconds for all valves remaining, including the active valve, if any + optional time_remaining_current_operation(); /// returns a pointer to a valve's control switch object SprinklerControllerSwitch *control_switch(size_t valve_number); @@ -371,9 +434,13 @@ class Sprinkler : public Component, public EntityBase { /// returns true if valve's cycle is flagged as complete bool valve_cycle_complete_(size_t valve_number); - /// returns the number of the next/previous valve in the vector - size_t next_valve_number_(size_t first_valve); - size_t previous_valve_number_(size_t first_valve); + /// returns the number of the next valve in the vector or nullopt if no valves match criteria + optional next_valve_number_(optional first_valve = nullopt, bool include_disabled = true, + bool include_complete = true); + + /// returns the number of the previous valve in the vector or nullopt if no valves match criteria + optional previous_valve_number_(optional first_valve = nullopt, bool include_disabled = true, + bool include_complete = true); /// returns the number of the next valve that should be activated in a full cycle. /// if no valve is next (cycle is complete), returns no value (check with 'has_value()') @@ -385,11 +452,6 @@ class Sprinkler : public Component, public EntityBase { /// if no valve is next (for example, a full cycle is complete), next_req_ is reset via reset(). void load_next_valve_run_request_(optional first_valve = nullopt); - /// returns the number of the next/previous valve that should be activated. - /// if no valve is next (cycle is complete), returns no value (check with 'has_value()') - optional next_enabled_incomplete_valve_number_(optional first_valve); - optional previous_enabled_incomplete_valve_number_(optional first_valve); - /// returns true if any valve is enabled bool any_valve_is_enabled_(); @@ -424,7 +486,10 @@ class Sprinkler : public Component, public EntityBase { /// starts up the system from IDLE state void fsm_transition_to_shutdown_(); - /// return the current FSM state as a string + /// return the specified SprinklerValveRunRequestOrigin as a string + std::string req_as_str_(SprinklerValveRunRequestOrigin origin); + + /// return the specified SprinklerState state as a string std::string state_as_str_(SprinklerState state); /// Start/cancel/get status of valve timers @@ -446,6 +511,9 @@ class Sprinkler : public Component, public EntityBase { /// Maximum allowed queue size const uint8_t max_queue_size_{100}; + /// When set to true, the next and previous actions will skip disabled valves + bool next_prev_ignore_disabled_{false}; + /// Pump should be off during valve_open_delay interval bool pump_switch_off_during_valve_open_delay_{false}; @@ -518,12 +586,19 @@ class Sprinkler : public Component, public EntityBase { SprinklerControllerSwitch *controller_sw_{nullptr}; SprinklerControllerSwitch *queue_enable_sw_{nullptr}; SprinklerControllerSwitch *reverse_sw_{nullptr}; + SprinklerControllerSwitch *standby_sw_{nullptr}; + + /// Number components we'll present to the front end + SprinklerControllerNumber *multiplier_number_{nullptr}; + SprinklerControllerNumber *repeat_number_{nullptr}; std::unique_ptr> sprinkler_shutdown_action_; + std::unique_ptr> sprinkler_standby_shutdown_action_; std::unique_ptr> sprinkler_resumeorstart_action_; std::unique_ptr> sprinkler_turn_off_automation_; std::unique_ptr> sprinkler_turn_on_automation_; + std::unique_ptr> sprinkler_standby_turn_on_automation_; }; } // namespace sprinkler