diff --git a/CODEOWNERS b/CODEOWNERS index 0a9be8364d..fae9334c24 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -193,6 +193,7 @@ esphome/components/smt100/* @piechade esphome/components/socket/* @esphome/core esphome/components/sonoff_d1/* @anatoly-savchenkov esphome/components/spi/* @esphome/core +esphome/components/sprinkler/* @kbx81 esphome/components/sps30/* @martgras esphome/components/ssd1322_base/* @kbx81 esphome/components/ssd1322_spi/* @kbx81 diff --git a/esphome/components/sprinkler/__init__.py b/esphome/components/sprinkler/__init__.py new file mode 100644 index 0000000000..659eb5b58e --- /dev/null +++ b/esphome/components/sprinkler/__init__.py @@ -0,0 +1,599 @@ +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 switch +from esphome.const import ( + CONF_ID, + CONF_NAME, + CONF_REPEAT, + CONF_RUN_DURATION, +) + +AUTO_LOAD = ["switch"] +CODEOWNERS = ["@kbx81"] + +CONF_AUTO_ADVANCE_SWITCH = "auto_advance_switch" +CONF_ENABLE_SWITCH = "enable_switch" +CONF_MAIN_SWITCH = "main_switch" +CONF_MANUAL_SELECTION_DELAY = "manual_selection_delay" +CONF_MULTIPLIER = "multiplier" +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" +CONF_PUMP_START_PUMP_DELAY = "pump_start_pump_delay" +CONF_PUMP_START_VALVE_DELAY = "pump_start_valve_delay" +CONF_PUMP_STOP_PUMP_DELAY = "pump_stop_pump_delay" +CONF_PUMP_STOP_VALVE_DELAY = "pump_stop_valve_delay" +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_REVERSE_SWITCH = "reverse_switch" +CONF_VALVE_NUMBER = "valve_number" +CONF_VALVE_OPEN_DELAY = "valve_open_delay" +CONF_VALVE_OVERLAP = "valve_overlap" +CONF_VALVE_PULSE_DURATION = "valve_pulse_duration" +CONF_VALVE_OFF_SWITCH_ID = "valve_off_switch_id" +CONF_VALVE_ON_SWITCH_ID = "valve_on_switch_id" +CONF_VALVE_SWITCH = "valve_switch" +CONF_VALVE_SWITCH_ID = "valve_switch_id" +CONF_VALVES = "valves" + +sprinkler_ns = cg.esphome_ns.namespace("sprinkler") +Sprinkler = sprinkler_ns.class_("Sprinkler", cg.Component) +SprinklerControllerSwitch = sprinkler_ns.class_( + "SprinklerControllerSwitch", switch.Switch, cg.Component +) + +SetMultiplierAction = sprinkler_ns.class_("SetMultiplierAction", automation.Action) +QueueValveAction = sprinkler_ns.class_("QueueValveAction", automation.Action) +ClearQueuedValvesAction = sprinkler_ns.class_( + "ClearQueuedValvesAction", automation.Action +) +SetRepeatAction = sprinkler_ns.class_("SetRepeatAction", automation.Action) +SetRunDurationAction = sprinkler_ns.class_("SetRunDurationAction", automation.Action) +StartFromQueueAction = sprinkler_ns.class_("StartFromQueueAction", automation.Action) +StartFullCycleAction = sprinkler_ns.class_("StartFullCycleAction", automation.Action) +StartSingleValveAction = sprinkler_ns.class_( + "StartSingleValveAction", automation.Action +) +ShutdownAction = sprinkler_ns.class_("ShutdownAction", automation.Action) +NextValveAction = sprinkler_ns.class_("NextValveAction", automation.Action) +PreviousValveAction = sprinkler_ns.class_("PreviousValveAction", automation.Action) +PauseAction = sprinkler_ns.class_("PauseAction", automation.Action) +ResumeAction = sprinkler_ns.class_("ResumeAction", automation.Action) +ResumeOrStartAction = sprinkler_ns.class_("ResumeOrStartAction", automation.Action) + + +def validate_sprinkler(config): + for sprinkler_controller_index, sprinkler_controller in enumerate(config): + if len(sprinkler_controller[CONF_VALVES]) <= 1: + exclusions = [ + CONF_VALVE_OPEN_DELAY, + CONF_VALVE_OVERLAP, + CONF_AUTO_ADVANCE_SWITCH, + CONF_MAIN_SWITCH, + CONF_REVERSE_SWITCH, + ] + for config_item in exclusions: + if config_item in sprinkler_controller: + raise cv.Invalid(f"Do not define {config_item} with only one valve") + if CONF_ENABLE_SWITCH in sprinkler_controller[CONF_VALVES][0]: + raise cv.Invalid( + f"Do not define {CONF_ENABLE_SWITCH} with only one valve" + ) + else: + requirements = [ + CONF_AUTO_ADVANCE_SWITCH, + CONF_MAIN_SWITCH, + ] + for config_item in requirements: + if config_item not in sprinkler_controller: + raise cv.Invalid( + f"{config_item} is a required option for {sprinkler_controller_index}" + ) + + if ( + CONF_PUMP_SWITCH_OFF_DURING_VALVE_OPEN_DELAY in sprinkler_controller + and CONF_VALVE_OPEN_DELAY not in sprinkler_controller + ): + if sprinkler_controller[CONF_PUMP_SWITCH_OFF_DURING_VALVE_OPEN_DELAY]: + raise cv.Invalid( + f"{CONF_VALVE_OPEN_DELAY} must be defined when {CONF_PUMP_SWITCH_OFF_DURING_VALVE_OPEN_DELAY} is enabled" + ) + + for valve in sprinkler_controller[CONF_VALVES]: + if ( + CONF_VALVE_OVERLAP in sprinkler_controller + and valve[CONF_RUN_DURATION] <= sprinkler_controller[CONF_VALVE_OVERLAP] + ): + raise cv.Invalid( + f"{CONF_RUN_DURATION} must be greater than {CONF_VALVE_OVERLAP}" + ) + if ( + CONF_VALVE_OPEN_DELAY in sprinkler_controller + and valve[CONF_RUN_DURATION] + <= sprinkler_controller[CONF_VALVE_OPEN_DELAY] + ): + raise cv.Invalid( + f"{CONF_RUN_DURATION} must be greater than {CONF_VALVE_OPEN_DELAY}" + ) + if ( + CONF_PUMP_OFF_SWITCH_ID in valve and CONF_PUMP_ON_SWITCH_ID not in valve + ) or ( + CONF_PUMP_ON_SWITCH_ID in valve and CONF_PUMP_OFF_SWITCH_ID not in valve + ): + raise cv.Invalid( + f"Both {CONF_PUMP_OFF_SWITCH_ID} and {CONF_PUMP_ON_SWITCH_ID} must be specified for latching pump configuration" + ) + if CONF_PUMP_SWITCH_ID in valve and ( + CONF_PUMP_OFF_SWITCH_ID in valve or CONF_PUMP_ON_SWITCH_ID in valve + ): + raise cv.Invalid( + f"Do not specify {CONF_PUMP_OFF_SWITCH_ID} or {CONF_PUMP_ON_SWITCH_ID} when using {CONF_PUMP_SWITCH_ID}" + ) + if CONF_PUMP_PULSE_DURATION not in sprinkler_controller and ( + CONF_PUMP_OFF_SWITCH_ID in valve or CONF_PUMP_ON_SWITCH_ID in valve + ): + raise cv.Invalid( + f"{CONF_PUMP_PULSE_DURATION} must be specified when using {CONF_PUMP_OFF_SWITCH_ID} and {CONF_PUMP_ON_SWITCH_ID}" + ) + if ( + CONF_VALVE_OFF_SWITCH_ID in valve + and CONF_VALVE_ON_SWITCH_ID not in valve + ) or ( + CONF_VALVE_ON_SWITCH_ID in valve + and CONF_VALVE_OFF_SWITCH_ID not in valve + ): + raise cv.Invalid( + f"Both {CONF_VALVE_OFF_SWITCH_ID} and {CONF_VALVE_ON_SWITCH_ID} must be specified for latching valve configuration" + ) + if CONF_VALVE_SWITCH_ID in valve and ( + CONF_VALVE_OFF_SWITCH_ID in valve or CONF_VALVE_ON_SWITCH_ID in valve + ): + raise cv.Invalid( + f"Do not specify {CONF_VALVE_OFF_SWITCH_ID} or {CONF_VALVE_ON_SWITCH_ID} when using {CONF_VALVE_SWITCH_ID}" + ) + if CONF_VALVE_PULSE_DURATION not in sprinkler_controller and ( + CONF_VALVE_OFF_SWITCH_ID in valve or CONF_VALVE_ON_SWITCH_ID in valve + ): + raise cv.Invalid( + f"{CONF_VALVE_PULSE_DURATION} must be specified when using {CONF_VALVE_OFF_SWITCH_ID} and {CONF_VALVE_ON_SWITCH_ID}" + ) + if ( + CONF_VALVE_SWITCH_ID not in valve + and CONF_VALVE_OFF_SWITCH_ID not in valve + and CONF_VALVE_ON_SWITCH_ID not in valve + ): + 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" + ) + return config + + +SPRINKLER_ACTION_SCHEMA = maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(Sprinkler), + } +) + +SPRINKLER_ACTION_REPEAT_SCHEMA = cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(Sprinkler), + cv.Required(CONF_REPEAT): cv.templatable(cv.positive_int), + }, + key=CONF_REPEAT, +) + +SPRINKLER_ACTION_SINGLE_VALVE_SCHEMA = cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(Sprinkler), + cv.Required(CONF_VALVE_NUMBER): cv.templatable(cv.positive_int), + }, + key=CONF_VALVE_NUMBER, +) + +SPRINKLER_ACTION_SET_MULTIPLIER_SCHEMA = cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(Sprinkler), + cv.Required(CONF_MULTIPLIER): cv.templatable(cv.positive_float), + }, + key=CONF_MULTIPLIER, +) + +SPRINKLER_ACTION_SET_RUN_DURATION_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(Sprinkler), + cv.Required(CONF_RUN_DURATION): cv.templatable(cv.positive_time_period_seconds), + cv.Required(CONF_VALVE_NUMBER): cv.templatable(cv.positive_int), + } +) + +SPRINKLER_ACTION_QUEUE_VALVE_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(Sprinkler), + cv.Optional(CONF_RUN_DURATION, default=0): cv.templatable( + cv.positive_time_period_seconds + ), + cv.Required(CONF_VALVE_NUMBER): cv.templatable(cv.positive_int), + } +) + +SPRINKLER_VALVE_SCHEMA = cv.Schema( + { + cv.Optional(CONF_ENABLE_SWITCH): cv.maybe_simple_value( + switch.SWITCH_SCHEMA.extend( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SprinklerControllerSwitch), + } + ) + ), + key=CONF_NAME, + ), + 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.Required(CONF_VALVE_SWITCH): cv.maybe_simple_value( + switch.SWITCH_SCHEMA.extend( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SprinklerControllerSwitch), + } + ) + ), + key=CONF_NAME, + ), + cv.Optional(CONF_VALVE_OFF_SWITCH_ID): cv.use_id(switch.Switch), + cv.Optional(CONF_VALVE_ON_SWITCH_ID): cv.use_id(switch.Switch), + cv.Optional(CONF_VALVE_SWITCH_ID): cv.use_id(switch.Switch), + } +) + +SPRINKLER_CONTROLLER_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(Sprinkler), + cv.Optional(CONF_AUTO_ADVANCE_SWITCH): cv.maybe_simple_value( + switch.SWITCH_SCHEMA.extend( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SprinklerControllerSwitch), + } + ) + ), + key=CONF_NAME, + ), + cv.Optional(CONF_MAIN_SWITCH): cv.maybe_simple_value( + switch.SWITCH_SCHEMA.extend( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SprinklerControllerSwitch), + } + ) + ), + key=CONF_NAME, + ), + cv.Optional(CONF_QUEUE_ENABLE_SWITCH): cv.maybe_simple_value( + switch.SWITCH_SCHEMA.extend( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SprinklerControllerSwitch), + } + ) + ), + key=CONF_NAME, + ), + cv.Optional(CONF_REVERSE_SWITCH): cv.maybe_simple_value( + switch.SWITCH_SCHEMA.extend( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SprinklerControllerSwitch), + } + ) + ), + key=CONF_NAME, + ), + cv.Optional(CONF_MANUAL_SELECTION_DELAY): cv.positive_time_period_seconds, + cv.Optional(CONF_REPEAT): cv.positive_int, + cv.Optional(CONF_PUMP_PULSE_DURATION): cv.positive_time_period_milliseconds, + cv.Optional(CONF_VALVE_PULSE_DURATION): cv.positive_time_period_milliseconds, + cv.Exclusive( + CONF_PUMP_START_PUMP_DELAY, "pump_start_xxxx_delay" + ): cv.positive_time_period_seconds, + cv.Exclusive( + CONF_PUMP_STOP_PUMP_DELAY, "pump_stop_xxxx_delay" + ): cv.positive_time_period_seconds, + cv.Optional(CONF_PUMP_SWITCH_OFF_DURING_VALVE_OPEN_DELAY): cv.boolean, + cv.Exclusive( + CONF_PUMP_START_VALVE_DELAY, "pump_start_xxxx_delay" + ): cv.positive_time_period_seconds, + cv.Exclusive( + CONF_PUMP_STOP_VALVE_DELAY, "pump_stop_xxxx_delay" + ): cv.positive_time_period_seconds, + cv.Exclusive( + CONF_VALVE_OVERLAP, "open_delay/overlap" + ): cv.positive_time_period_seconds, + cv.Exclusive( + CONF_VALVE_OPEN_DELAY, "open_delay/overlap" + ): cv.positive_time_period_seconds, + cv.Required(CONF_VALVES): cv.ensure_list(SPRINKLER_VALVE_SCHEMA), + } +).extend(cv.ENTITY_BASE_SCHEMA) + +CONFIG_SCHEMA = cv.All( + cv.ensure_list(SPRINKLER_CONTROLLER_SCHEMA), + validate_sprinkler, +) + + +@automation.register_action( + "sprinkler.set_multiplier", + SetMultiplierAction, + SPRINKLER_ACTION_SET_MULTIPLIER_SCHEMA, +) +async def sprinkler_set_multiplier_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_MULTIPLIER], args, cg.float_) + cg.add(var.set_multiplier(template_)) + return var + + +@automation.register_action( + "sprinkler.queue_valve", + QueueValveAction, + SPRINKLER_ACTION_QUEUE_VALVE_SCHEMA, +) +async def sprinkler_set_queued_valve_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_VALVE_NUMBER], args, cg.uint8) + cg.add(var.set_valve_number(template_)) + template_ = await cg.templatable(config[CONF_RUN_DURATION], args, cg.uint32) + cg.add(var.set_valve_run_duration(template_)) + return var + + +@automation.register_action( + "sprinkler.set_repeat", + SetRepeatAction, + SPRINKLER_ACTION_REPEAT_SCHEMA, +) +async def sprinkler_set_repeat_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_REPEAT], args, cg.float_) + cg.add(var.set_repeat(template_)) + return var + + +@automation.register_action( + "sprinkler.set_valve_run_duration", + SetRunDurationAction, + SPRINKLER_ACTION_SET_RUN_DURATION_SCHEMA, +) +async def sprinkler_set_valve_run_duration_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_VALVE_NUMBER], args, cg.uint8) + cg.add(var.set_valve_number(template_)) + template_ = await cg.templatable(config[CONF_RUN_DURATION], args, cg.uint32) + cg.add(var.set_valve_run_duration(template_)) + return var + + +@automation.register_action( + "sprinkler.start_from_queue", StartFromQueueAction, SPRINKLER_ACTION_SCHEMA +) +async def sprinkler_start_from_queue_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + + +@automation.register_action( + "sprinkler.start_full_cycle", StartFullCycleAction, SPRINKLER_ACTION_SCHEMA +) +async def sprinkler_start_full_cycle_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + + +@automation.register_action( + "sprinkler.start_single_valve", + StartSingleValveAction, + SPRINKLER_ACTION_SINGLE_VALVE_SCHEMA, +) +async def sprinkler_start_single_valve_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_VALVE_NUMBER], args, cg.uint8) + cg.add(var.set_valve_to_start(template_)) + return var + + +@automation.register_action( + "sprinkler.clear_queued_valves", ClearQueuedValvesAction, SPRINKLER_ACTION_SCHEMA +) +@automation.register_action( + "sprinkler.next_valve", NextValveAction, SPRINKLER_ACTION_SCHEMA +) +@automation.register_action( + "sprinkler.previous_valve", PreviousValveAction, SPRINKLER_ACTION_SCHEMA +) +@automation.register_action("sprinkler.pause", PauseAction, SPRINKLER_ACTION_SCHEMA) +@automation.register_action("sprinkler.resume", ResumeAction, SPRINKLER_ACTION_SCHEMA) +@automation.register_action( + "sprinkler.resume_or_start_full_cycle", ResumeOrStartAction, SPRINKLER_ACTION_SCHEMA +) +@automation.register_action( + "sprinkler.shutdown", ShutdownAction, SPRINKLER_ACTION_SCHEMA +) +async def sprinkler_simple_action_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + + +async def to_code(config): + for sprinkler_controller in config: + if len(sprinkler_controller[CONF_VALVES]) > 1: + var = cg.new_Pvariable( + sprinkler_controller[CONF_ID], + sprinkler_controller[CONF_MAIN_SWITCH][CONF_NAME], + ) + else: + var = cg.new_Pvariable( + sprinkler_controller[CONF_ID], + sprinkler_controller[CONF_VALVES][0][CONF_VALVE_SWITCH][CONF_NAME], + ) + await cg.register_component(var, sprinkler_controller) + + if len(sprinkler_controller[CONF_VALVES]) > 1: + sw_var = await switch.new_switch(sprinkler_controller[CONF_MAIN_SWITCH]) + await cg.register_component(sw_var, sprinkler_controller[CONF_MAIN_SWITCH]) + cg.add(var.set_controller_main_switch(sw_var)) + + sw_aa_var = await switch.new_switch( + sprinkler_controller[CONF_AUTO_ADVANCE_SWITCH] + ) + await cg.register_component( + sw_aa_var, sprinkler_controller[CONF_AUTO_ADVANCE_SWITCH] + ) + cg.add(var.set_controller_auto_adv_switch(sw_aa_var)) + + if CONF_QUEUE_ENABLE_SWITCH in sprinkler_controller: + sw_qen_var = await switch.new_switch( + sprinkler_controller[CONF_QUEUE_ENABLE_SWITCH] + ) + await cg.register_component( + sw_qen_var, sprinkler_controller[CONF_QUEUE_ENABLE_SWITCH] + ) + cg.add(var.set_controller_queue_enable_switch(sw_qen_var)) + + if CONF_REVERSE_SWITCH in sprinkler_controller: + sw_rev_var = await switch.new_switch( + sprinkler_controller[CONF_REVERSE_SWITCH] + ) + await cg.register_component( + sw_rev_var, sprinkler_controller[CONF_REVERSE_SWITCH] + ) + cg.add(var.set_controller_reverse_switch(sw_rev_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]) + + if ( + CONF_ENABLE_SWITCH in valve + and len(sprinkler_controller[CONF_VALVES]) > 1 + ): + sw_en_var = await switch.new_switch(valve[CONF_ENABLE_SWITCH]) + await cg.register_component(sw_en_var, valve[CONF_ENABLE_SWITCH]) + + cg.add(var.add_valve(sw_valve_var, sw_en_var)) + else: + cg.add(var.add_valve(sw_valve_var)) + + if CONF_MANUAL_SELECTION_DELAY in sprinkler_controller: + cg.add( + var.set_manual_selection_delay( + sprinkler_controller[CONF_MANUAL_SELECTION_DELAY] + ) + ) + + if CONF_REPEAT in sprinkler_controller: + cg.add(var.set_repeat(sprinkler_controller[CONF_REPEAT])) + + if CONF_VALVE_OVERLAP in sprinkler_controller: + cg.add(var.set_valve_overlap(sprinkler_controller[CONF_VALVE_OVERLAP])) + + if CONF_VALVE_OPEN_DELAY in sprinkler_controller: + cg.add( + var.set_valve_open_delay(sprinkler_controller[CONF_VALVE_OPEN_DELAY]) + ) + + if CONF_PUMP_START_PUMP_DELAY in sprinkler_controller: + cg.add( + var.set_pump_start_delay( + sprinkler_controller[CONF_PUMP_START_PUMP_DELAY] + ) + ) + + if CONF_PUMP_STOP_PUMP_DELAY in sprinkler_controller: + cg.add( + var.set_pump_stop_delay(sprinkler_controller[CONF_PUMP_STOP_PUMP_DELAY]) + ) + + if CONF_PUMP_START_VALVE_DELAY in sprinkler_controller: + cg.add( + var.set_valve_start_delay( + sprinkler_controller[CONF_PUMP_START_VALVE_DELAY] + ) + ) + + if CONF_PUMP_STOP_VALVE_DELAY in sprinkler_controller: + cg.add( + var.set_valve_stop_delay( + sprinkler_controller[CONF_PUMP_STOP_VALVE_DELAY] + ) + ) + + if CONF_PUMP_SWITCH_OFF_DURING_VALVE_OPEN_DELAY in sprinkler_controller: + cg.add( + var.set_pump_switch_off_during_valve_open_delay( + sprinkler_controller[CONF_PUMP_SWITCH_OFF_DURING_VALVE_OPEN_DELAY] + ) + ) + + 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_VALVE_SWITCH_ID in valve: + valve_switch = await cg.get_variable(valve[CONF_VALVE_SWITCH_ID]) + cg.add( + var.configure_valve_switch( + valve_index, valve_switch, valve[CONF_RUN_DURATION] + ) + ) + elif CONF_VALVE_OFF_SWITCH_ID in valve and CONF_VALVE_ON_SWITCH_ID in valve: + valve_switch_off = await cg.get_variable( + valve[CONF_VALVE_OFF_SWITCH_ID] + ) + valve_switch_on = await cg.get_variable(valve[CONF_VALVE_ON_SWITCH_ID]) + cg.add( + var.configure_valve_switch_pulsed( + valve_index, + valve_switch_off, + valve_switch_on, + sprinkler_controller[CONF_VALVE_PULSE_DURATION], + valve[CONF_RUN_DURATION], + ) + ) + + if CONF_PUMP_SWITCH_ID in valve: + pump = await cg.get_variable(valve[CONF_PUMP_SWITCH_ID]) + cg.add(var.configure_valve_pump_switch(valve_index, pump)) + elif CONF_PUMP_OFF_SWITCH_ID in valve and CONF_PUMP_ON_SWITCH_ID in valve: + pump_off = await cg.get_variable(valve[CONF_PUMP_OFF_SWITCH_ID]) + pump_on = await cg.get_variable(valve[CONF_PUMP_ON_SWITCH_ID]) + cg.add( + var.configure_valve_pump_switch_pulsed( + valve_index, + pump_off, + pump_on, + sprinkler_controller[CONF_PUMP_PULSE_DURATION], + ) + ) + + for sprinkler_controller in config: + var = await cg.get_variable(sprinkler_controller[CONF_ID]) + for controller_to_add in config: + if sprinkler_controller[CONF_ID] != controller_to_add[CONF_ID]: + cg.add( + var.add_controller( + await cg.get_variable(controller_to_add[CONF_ID]) + ) + ) diff --git a/esphome/components/sprinkler/automation.h b/esphome/components/sprinkler/automation.h new file mode 100644 index 0000000000..dd0ea44633 --- /dev/null +++ b/esphome/components/sprinkler/automation.h @@ -0,0 +1,169 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/components/sprinkler/sprinkler.h" + +namespace esphome { +namespace sprinkler { + +template class SetMultiplierAction : public Action { + public: + explicit SetMultiplierAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} + + TEMPLATABLE_VALUE(float, multiplier) + + void play(Ts... x) override { this->sprinkler_->set_multiplier(this->multiplier_.optional_value(x...)); } + + protected: + Sprinkler *sprinkler_; +}; + +template class QueueValveAction : public Action { + public: + explicit QueueValveAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} + + TEMPLATABLE_VALUE(size_t, valve_number) + TEMPLATABLE_VALUE(uint32_t, valve_run_duration) + + void play(Ts... x) override { + this->sprinkler_->queue_valve(this->valve_number_.optional_value(x...), + this->valve_run_duration_.optional_value(x...)); + } + + protected: + Sprinkler *sprinkler_; +}; + +template class ClearQueuedValvesAction : public Action { + public: + explicit ClearQueuedValvesAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} + + void play(Ts... x) override { this->sprinkler_->clear_queued_valves(); } + + protected: + Sprinkler *sprinkler_; +}; + +template class SetRepeatAction : public Action { + public: + explicit SetRepeatAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} + + TEMPLATABLE_VALUE(uint32_t, repeat) + + void play(Ts... x) override { this->sprinkler_->set_repeat(this->repeat_.optional_value(x...)); } + + protected: + Sprinkler *sprinkler_; +}; + +template class SetRunDurationAction : public Action { + public: + explicit SetRunDurationAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} + + TEMPLATABLE_VALUE(size_t, valve_number) + TEMPLATABLE_VALUE(uint32_t, valve_run_duration) + + void play(Ts... x) override { + this->sprinkler_->set_valve_run_duration(this->valve_number_.optional_value(x...), + this->valve_run_duration_.optional_value(x...)); + } + + protected: + Sprinkler *sprinkler_; +}; + +template class StartFromQueueAction : public Action { + public: + explicit StartFromQueueAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} + + void play(Ts... x) override { this->sprinkler_->start_from_queue(); } + + protected: + Sprinkler *sprinkler_; +}; + +template class StartFullCycleAction : public Action { + public: + explicit StartFullCycleAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} + + void play(Ts... x) override { this->sprinkler_->start_full_cycle(); } + + protected: + Sprinkler *sprinkler_; +}; + +template class StartSingleValveAction : public Action { + public: + explicit StartSingleValveAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} + + TEMPLATABLE_VALUE(size_t, valve_to_start) + + void play(Ts... x) override { this->sprinkler_->start_single_valve(this->valve_to_start_.optional_value(x...)); } + + protected: + Sprinkler *sprinkler_; +}; + +template class ShutdownAction : public Action { + public: + explicit ShutdownAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} + + void play(Ts... x) override { this->sprinkler_->shutdown(); } + + protected: + Sprinkler *sprinkler_; +}; + +template class NextValveAction : public Action { + public: + explicit NextValveAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} + + void play(Ts... x) override { this->sprinkler_->next_valve(); } + + protected: + Sprinkler *sprinkler_; +}; + +template class PreviousValveAction : public Action { + public: + explicit PreviousValveAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} + + void play(Ts... x) override { this->sprinkler_->previous_valve(); } + + protected: + Sprinkler *sprinkler_; +}; + +template class PauseAction : public Action { + public: + explicit PauseAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} + + void play(Ts... x) override { this->sprinkler_->pause(); } + + protected: + Sprinkler *sprinkler_; +}; + +template class ResumeAction : public Action { + public: + explicit ResumeAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} + + void play(Ts... x) override { this->sprinkler_->resume(); } + + protected: + Sprinkler *sprinkler_; +}; + +template class ResumeOrStartAction : public Action { + public: + explicit ResumeOrStartAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} + + void play(Ts... x) override { this->sprinkler_->resume_or_start_full_cycle(); } + + protected: + Sprinkler *sprinkler_; +}; + +} // namespace sprinkler +} // namespace esphome diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp new file mode 100644 index 0000000000..ab694c8412 --- /dev/null +++ b/esphome/components/sprinkler/sprinkler.cpp @@ -0,0 +1,1347 @@ +#include "automation.h" +#include "sprinkler.h" + +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include + +namespace esphome { +namespace sprinkler { + +static const char *const TAG = "sprinkler"; + +SprinklerSwitch::SprinklerSwitch() {} +SprinklerSwitch::SprinklerSwitch(switch_::Switch *sprinkler_switch) : on_switch_(sprinkler_switch) {} +SprinklerSwitch::SprinklerSwitch(switch_::Switch *off_switch, switch_::Switch *on_switch, uint32_t pulse_duration) + : pulse_duration_(pulse_duration), off_switch_(off_switch), on_switch_(on_switch) {} + +bool SprinklerSwitch::is_latching_valve() { return (this->off_switch_ != nullptr) && (this->on_switch_ != nullptr); } + +void SprinklerSwitch::loop() { + if ((this->pinned_millis_) && (millis() > this->pinned_millis_ + this->pulse_duration_)) { + this->pinned_millis_ = 0; // reset tracker + if (this->off_switch_->state) { + this->off_switch_->turn_off(); + } + if (this->on_switch_->state) { + this->on_switch_->turn_off(); + } + } +} + +void SprinklerSwitch::turn_off() { + if (!this->state()) { // do nothing if we're already in the requested state + return; + } + if (this->off_switch_ != nullptr) { // latching valve, start a pulse + if (!this->off_switch_->state) { + this->off_switch_->turn_on(); + } + this->pinned_millis_ = millis(); + } else if (this->on_switch_ != nullptr) { // non-latching valve + this->on_switch_->turn_off(); + } + this->state_ = false; +} + +void SprinklerSwitch::turn_on() { + if (this->state()) { // do nothing if we're already in the requested state + return; + } + if (this->off_switch_ != nullptr) { // latching valve, start a pulse + if (!this->on_switch_->state) { + this->on_switch_->turn_on(); + } + this->pinned_millis_ = millis(); + } else if (this->on_switch_ != nullptr) { // non-latching valve + this->on_switch_->turn_on(); + } + this->state_ = true; +} + +bool SprinklerSwitch::state() { + if ((this->off_switch_ == nullptr) && (this->on_switch_ != nullptr)) { // latching valve is not configured... + return this->on_switch_->state; // ...so just return the pump switch state + } + return this->state_; +} + +void SprinklerSwitch::sync_valve_state(bool latch_state) { + if (this->is_latching_valve()) { + this->state_ = latch_state; + } else if (this->on_switch_ != nullptr) { + this->state_ = this->on_switch_->state; + } +} + +SprinklerControllerSwitch::SprinklerControllerSwitch() + : turn_on_trigger_(new Trigger<>()), turn_off_trigger_(new Trigger<>()) {} + +void SprinklerControllerSwitch::loop() { + if (!this->f_.has_value()) + return; + auto s = (*this->f_)(); + if (!s.has_value()) + return; + + this->publish_state(*s); +} + +void SprinklerControllerSwitch::write_state(bool state) { + if (this->prev_trigger_ != nullptr) { + this->prev_trigger_->stop_action(); + } + + if (state) { + this->prev_trigger_ = this->turn_on_trigger_; + this->turn_on_trigger_->trigger(); + } else { + this->prev_trigger_ = this->turn_off_trigger_; + this->turn_off_trigger_->trigger(); + } + + if (this->optimistic_) + 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; } + +Trigger<> *SprinklerControllerSwitch::get_turn_on_trigger() const { return this->turn_on_trigger_; } +Trigger<> *SprinklerControllerSwitch::get_turn_off_trigger() const { return this->turn_off_trigger_; } + +void SprinklerControllerSwitch::setup() { + if (!this->restore_state_) + return; + + auto restored = this->get_initial_state(); + if (!restored.has_value()) + return; + + ESP_LOGD(TAG, " Restored state %s", ONOFF(*restored)); + if (*restored) { + 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; } + +SprinklerValveOperator::SprinklerValveOperator() {} +SprinklerValveOperator::SprinklerValveOperator(SprinklerValve *valve, Sprinkler *controller) + : controller_(controller), valve_(valve) {} + +void SprinklerValveOperator::loop() { + if (millis() >= this->pinned_millis_) { // dummy check + switch (this->state_) { + case STARTING: + if (millis() > (this->pinned_millis_ + this->start_delay_)) { + this->run_(); // start_delay_ has been exceeded, so ensure both valves are on and update the state + } + break; + + case ACTIVE: + if (millis() > (this->pinned_millis_ + this->start_delay_ + this->run_duration_)) { + this->stop(); // start_delay_ + run_duration_ has been exceeded, start shutting down + } + break; + + case STOPPING: + if (millis() > (this->pinned_millis_ + this->stop_delay_)) { + this->kill_(); // stop_delay_has been exceeded, ensure all valves are off + } + break; + + default: + break; + } + } else { // perhaps millis() rolled over...or something else is horribly wrong! + this->stop(); // bail out (TODO: handle this highly unlikely situation better...) + } +} + +void SprinklerValveOperator::set_controller(Sprinkler *controller) { + if (controller != nullptr) { + this->controller_ = controller; + } +} + +void SprinklerValveOperator::set_valve(SprinklerValve *valve) { + if (valve != nullptr) { + this->state_ = IDLE; // reset state + this->run_duration_ = 0; // reset to ensure the valve isn't started without updating it + this->pinned_millis_ = 0; // reset because (new) valve has not been started yet + this->kill_(); // ensure everything is off before we let go! + this->valve_ = valve; // finally, set the pointer to the new valve + } +} + +void SprinklerValveOperator::set_run_duration(uint32_t run_duration) { + if (run_duration) { + this->run_duration_ = run_duration * 1000; + } +} + +void SprinklerValveOperator::set_start_delay(uint32_t start_delay, bool start_delay_is_valve_delay) { + this->start_delay_is_valve_delay_ = start_delay_is_valve_delay; + this->start_delay_ = start_delay * 1000; // because 1000 milliseconds is one second +} + +void SprinklerValveOperator::set_stop_delay(uint32_t stop_delay, bool stop_delay_is_valve_delay) { + this->stop_delay_is_valve_delay_ = stop_delay_is_valve_delay; + this->stop_delay_ = stop_delay * 1000; // because 1000 milliseconds is one second +} + +void SprinklerValveOperator::start() { + if (!this->run_duration_) { // can't start if zero run duration + return; + } + if (this->start_delay_ && (this->pump_switch() != nullptr)) { + this->state_ = STARTING; // STARTING state requires both a pump and a start_delay_ + if (this->start_delay_is_valve_delay_) { + this->pump_on_(); + } else if (!this->pump_switch()->state()) { // if the pump is already on, wait to switch on the valve + this->valve_on_(); // to ensure consistent run time + } + } else { + this->run_(); // there is no start_delay_, so just start the pump and valve + } + this->pinned_millis_ = millis(); // save the time the start request was made +} + +void SprinklerValveOperator::stop() { + if ((this->state_ == IDLE) || (this->state_ == STOPPING)) { // can't stop if already stopped or stopping + return; + } + if (this->stop_delay_ && (this->pump_switch() != nullptr)) { + this->state_ = STOPPING; // STOPPING state requires both a pump and a stop_delay_ + if (this->stop_delay_is_valve_delay_) { + this->pump_off_(); + } else { + this->valve_off_(); + } + if (this->pump_switch()->state()) { // if the pump is still on at this point, it may be in use... + this->valve_off_(); // ...so just switch the valve off now to ensure consistent run time + } + this->pinned_millis_ = millis(); // save the time the stop request was made + } else { + this->kill_(); // there is no stop_delay_, so just stop the pump and valve + } +} + +uint32_t SprinklerValveOperator::run_duration() { return this->run_duration_; } + +uint32_t SprinklerValveOperator::time_remaining() { + if ((this->state_ == STARTING) || (this->state_ == ACTIVE)) { + return (this->pinned_millis_ + this->start_delay_ + this->run_duration_ - millis()) / 1000; + } + return 0; +} + +SprinklerState SprinklerValveOperator::state() { return this->state_; } + +SprinklerSwitch *SprinklerValveOperator::pump_switch() { + if ((this->controller_ == nullptr) || (this->valve_ == nullptr)) { + return nullptr; + } + if (this->valve_->pump_switch_index.has_value()) { + return this->controller_->valve_pump_switch_by_pump_index(this->valve_->pump_switch_index.value()); + } + return nullptr; +} + +void SprinklerValveOperator::pump_off_() { + if ((this->valve_ == nullptr) || (this->pump_switch() == nullptr)) { // safety first! + return; + } + if (this->controller_ == nullptr) { // safety first! + this->pump_switch()->turn_off(); // if no controller was set, just switch off the pump + } else { // ...otherwise, do it "safely" + auto state = this->state_; // this is silly, but... + this->state_ = BYPASS; // ...exclude me from the pump-in-use check that set_pump_state() does + this->controller_->set_pump_state(this->pump_switch(), false); + this->state_ = state; + } +} + +void SprinklerValveOperator::pump_on_() { + if ((this->valve_ == nullptr) || (this->pump_switch() == nullptr)) { // safety first! + return; + } + if (this->controller_ == nullptr) { // safety first! + this->pump_switch()->turn_on(); // if no controller was set, just switch on the pump + } else { // ...otherwise, do it "safely" + auto state = this->state_; // this is silly, but... + this->state_ = BYPASS; // ...exclude me from the pump-in-use check that set_pump_state() does + this->controller_->set_pump_state(this->pump_switch(), true); + this->state_ = state; + } +} + +void SprinklerValveOperator::valve_off_() { + if (this->valve_ == nullptr) { // safety first! + return; + } + if (this->valve_->valve_switch.state()) { + this->valve_->valve_switch.turn_off(); + } +} + +void SprinklerValveOperator::valve_on_() { + if (this->valve_ == nullptr) { // safety first! + return; + } + if (!this->valve_->valve_switch.state()) { + this->valve_->valve_switch.turn_on(); + } +} + +void SprinklerValveOperator::kill_() { + this->state_ = IDLE; + this->valve_off_(); + this->pump_off_(); +} + +void SprinklerValveOperator::run_() { + this->state_ = ACTIVE; + this->valve_on_(); + this->pump_on_(); +} + +SprinklerValveRunRequest::SprinklerValveRunRequest() {} +SprinklerValveRunRequest::SprinklerValveRunRequest(size_t valve_number, uint32_t run_duration, + SprinklerValveOperator *valve_op) + : valve_number_(valve_number), run_duration_(run_duration), valve_op_(valve_op) {} + +bool SprinklerValveRunRequest::has_request() { return this->has_valve_; } +bool SprinklerValveRunRequest::has_valve_operator() { return !(this->valve_op_ == nullptr); } + +void SprinklerValveRunRequest::set_run_duration(uint32_t run_duration) { this->run_duration_ = run_duration; } + +void SprinklerValveRunRequest::set_valve(size_t valve_number) { + this->valve_number_ = valve_number; + this->run_duration_ = 0; + this->valve_op_ = nullptr; + this->has_valve_ = true; +} + +void SprinklerValveRunRequest::set_valve_operator(SprinklerValveOperator *valve_op) { + if (valve_op != nullptr) { + this->valve_op_ = valve_op; + } +} + +void SprinklerValveRunRequest::reset() { + this->has_valve_ = false; + this->run_duration_ = 0; + this->valve_op_ = nullptr; +} + +uint32_t SprinklerValveRunRequest::run_duration() { return this->run_duration_; } + +size_t SprinklerValveRunRequest::valve() { return this->valve_number_; } + +optional SprinklerValveRunRequest::valve_as_opt() { + if (this->has_valve_) { + return this->valve_number_; + } + return nullopt; +} + +SprinklerValveOperator *SprinklerValveRunRequest::valve_operator() { return this->valve_op_; } + +Sprinkler::Sprinkler() {} +Sprinkler::Sprinkler(const std::string &name) : EntityBase(name) {} + +void Sprinkler::setup() { this->all_valves_off_(true); } + +void Sprinkler::loop() { + for (auto &p : this->pump_) { + p.loop(); + } + for (auto &v : this->valve_) { + v.valve_switch.loop(); + } + for (auto &vo : this->valve_op_) { + vo.loop(); + } +} + +void Sprinkler::add_valve(SprinklerControllerSwitch *valve_sw, SprinklerControllerSwitch *enable_sw) { + auto new_valve_number = this->number_of_valves(); + this->valve_.resize(new_valve_number + 1); + SprinklerValve *new_valve = &this->valve_[new_valve_number]; + + new_valve->controller_switch = valve_sw; + new_valve->controller_switch->set_state_lambda([=]() -> optional { + if (this->valve_pump_switch(new_valve_number) != nullptr) { + return this->valve_switch(new_valve_number)->state() && this->valve_pump_switch(new_valve_number)->state(); + } + return this->valve_switch(new_valve_number)->state(); + }); + + new_valve->valve_turn_off_automation = + make_unique>(new_valve->controller_switch->get_turn_off_trigger()); + new_valve->valve_shutdown_action = make_unique>(this); + new_valve->valve_turn_off_automation->add_actions({new_valve->valve_shutdown_action.get()}); + + new_valve->valve_turn_on_automation = make_unique>(new_valve->controller_switch->get_turn_on_trigger()); + new_valve->valve_resumeorstart_action = make_unique>(this); + new_valve->valve_resumeorstart_action->set_valve_to_start(new_valve_number); + new_valve->valve_turn_on_automation->add_actions({new_valve->valve_resumeorstart_action.get()}); + + 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); + } +} + +void Sprinkler::add_controller(Sprinkler *other_controller) { this->other_controllers_.push_back(other_controller); } + +void Sprinkler::set_controller_main_switch(SprinklerControllerSwitch *controller_switch) { + this->controller_sw_ = controller_switch; + controller_switch->set_state_lambda([=]() -> optional { + for (size_t valve_number = 0; valve_number < this->number_of_valves(); valve_number++) { + if (this->valve_[valve_number].controller_switch->state) { + return true; + } + } + return this->active_req_.has_request(); + }); + + this->sprinkler_turn_off_automation_ = make_unique>(controller_switch->get_turn_off_trigger()); + this->sprinkler_shutdown_action_ = make_unique>(this); + this->sprinkler_turn_off_automation_->add_actions({sprinkler_shutdown_action_.get()}); + + this->sprinkler_turn_on_automation_ = make_unique>(controller_switch->get_turn_on_trigger()); + this->sprinkler_resumeorstart_action_ = make_unique>(this); + this->sprinkler_turn_on_automation_->add_actions({sprinkler_resumeorstart_action_.get()}); +} + +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::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; + } +} + +void Sprinkler::configure_valve_switch_pulsed(size_t valve_number, switch_::Switch *valve_switch_off, + switch_::Switch *valve_switch_on, uint32_t pulse_duration, + uint32_t run_duration) { + if (this->is_a_valid_valve(valve_number)) { + this->valve_[valve_number].valve_switch.set_off_switch(valve_switch_off); + 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; + } +} + +void Sprinkler::configure_valve_pump_switch(size_t valve_number, switch_::Switch *pump_switch) { + if (this->is_a_valid_valve(valve_number)) { + for (size_t i = 0; i < this->pump_.size(); i++) { // check each existing registered pump + if (this->pump_[i].on_switch() == pump_switch) { // if the "new" pump matches one we already have... + this->valve_[valve_number].pump_switch_index = i; // ...save its index in the SprinklerSwitch vector pump_... + return; // ...and we are done + } + } // if we end up here, no pumps matched, so add a new one and set the valve's SprinklerSwitch at it + 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 + } +} + +void Sprinkler::configure_valve_pump_switch_pulsed(size_t valve_number, switch_::Switch *pump_switch_off, + switch_::Switch *pump_switch_on, uint32_t pulse_duration) { + if (this->is_a_valid_valve(valve_number)) { + for (size_t i = 0; i < this->pump_.size(); i++) { // check each existing registered pump + if ((this->pump_[i].off_switch() == pump_switch_off) && + (this->pump_[i].on_switch() == pump_switch_on)) { // if the "new" pump matches one we already have... + this->valve_[valve_number].pump_switch_index = i; // ...save its index in the SprinklerSwitch vector pump_... + return; // ...and we are done + } + } // if we end up here, no pumps matched, so add a new one and set the valve's SprinklerSwitch at it + this->pump_.resize(this->pump_.size() + 1); + this->pump_.back().set_off_switch(pump_switch_off); + 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 + } +} + +void Sprinkler::set_multiplier(const optional multiplier) { + if (multiplier.has_value()) { + if (multiplier.value() > 0) { + this->multiplier_ = multiplier.value(); + } + } +} + +void Sprinkler::set_pump_start_delay(uint32_t start_delay) { + this->start_delay_is_valve_delay_ = false; + this->start_delay_ = start_delay; +} + +void Sprinkler::set_pump_stop_delay(uint32_t stop_delay) { + this->stop_delay_is_valve_delay_ = false; + this->stop_delay_ = stop_delay; +} + +void Sprinkler::set_valve_start_delay(uint32_t start_delay) { + this->start_delay_is_valve_delay_ = true; + this->start_delay_ = start_delay; +} + +void Sprinkler::set_valve_stop_delay(uint32_t stop_delay) { + this->stop_delay_is_valve_delay_ = true; + this->stop_delay_ = stop_delay; +} + +void Sprinkler::set_pump_switch_off_during_valve_open_delay(bool pump_switch_off_during_valve_open_delay) { + this->pump_switch_off_during_valve_open_delay_ = pump_switch_off_during_valve_open_delay; +} + +void Sprinkler::set_valve_open_delay(const uint32_t valve_open_delay) { + if (valve_open_delay > 0) { + this->valve_overlap_ = false; + this->switching_delay_ = valve_open_delay; + } else { + this->switching_delay_.reset(); + } +} + +void Sprinkler::set_valve_overlap(uint32_t valve_overlap) { + if (valve_overlap > 0) { + this->valve_overlap_ = true; + this->switching_delay_ = valve_overlap; + } else { + this->switching_delay_.reset(); + } + this->pump_switch_off_during_valve_open_delay_ = false; // incompatible option +} + +void Sprinkler::set_manual_selection_delay(uint32_t manual_selection_delay) { + if (manual_selection_delay > 0) { + this->manual_selection_delay_ = manual_selection_delay; + } else { + this->manual_selection_delay_.reset(); + } +} + +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(); + } + } +} + +void Sprinkler::set_auto_advance(const bool auto_advance) { + if (this->auto_adv_sw_ != nullptr) { + this->auto_adv_sw_->publish_state(auto_advance); + } +} + +void Sprinkler::set_repeat(optional repeat) { this->target_repeats_ = repeat; } + +void Sprinkler::set_queue_enable(bool queue_enable) { + if (this->queue_enable_sw_ != nullptr) { + this->queue_enable_sw_->publish_state(queue_enable); + } +} + +void Sprinkler::set_reverse(const bool reverse) { + if (this->reverse_sw_ != nullptr) { + this->reverse_sw_->publish_state(reverse); + } +} + +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; + } + return 0; +} + +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 = 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)) { + return std::max(this->switching_delay_.value_or(0) * 2, std::max(this->start_delay_, this->stop_delay_)); + } + return run_duration; +} + +bool Sprinkler::auto_advance() { + if (this->auto_adv_sw_ != nullptr) { + return this->auto_adv_sw_->state; + } + return false; +} + +float Sprinkler::multiplier() { return this->multiplier_; } + +optional Sprinkler::repeat() { 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_; + } + } + return nullopt; +} + +bool Sprinkler::queue_enabled() { + if (this->queue_enable_sw_ != nullptr) { + return this->queue_enable_sw_->state; + } + return true; +} + +bool Sprinkler::reverse() { + if (this->reverse_sw_ != nullptr) { + return this->reverse_sw_->state; + } + return false; +} + +void Sprinkler::start_from_queue() { + if (this->queued_valves_.empty()) { + return; // if there is nothing in the queue, don't do anything + } + if (this->queue_enabled() && this->active_valve().has_value()) { + 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->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->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->prep_full_cycle_(); + this->repeat_count_ = 0; + // if there is no active valve already, start the first valve in the cycle + if (!this->active_req_.has_request()) { + this->fsm_kick_(); + } +} + +void Sprinkler::start_single_valve(const optional valve_number) { + 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->reset_cycle_states_(); // just in case auto-advance is switched on later + this->repeat_count_ = 0; + this->fsm_request_(valve_number.value()); +} + +void Sprinkler::queue_valve(optional valve_number, optional run_duration) { + if (valve_number.has_value()) { + if (this->is_a_valid_valve(valve_number.value()) && (this->queued_valves_.size() < this->max_queue_size_)) { + SprinklerQueueItem item{valve_number.value(), run_duration.value()}; + this->queued_valves_.insert(this->queued_valves_.begin(), item); + ESP_LOGD(TAG, "Valve %u placed into queue with run duration of %u seconds", valve_number.value_or(0), + run_duration.value_or(0)); + } + } +} + +void Sprinkler::clear_queued_valves() { + this->queued_valves_.clear(); + ESP_LOGD(TAG, "Queue cleared"); +} + +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))); + 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); + } else { + this->fsm_request_(this->manual_valve_.value()); + } +} + +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))); + 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); + } else { + this->fsm_request_(this->manual_valve_.value()); + } +} + +void Sprinkler::shutdown(bool clear_queue) { + this->cancel_timer_(sprinkler::TIMER_VALVE_SELECTION); + this->active_req_.reset(); + this->manual_valve_.reset(); + this->next_req_.reset(); + for (auto &vo : this->valve_op_) { + vo.stop(); + } + this->fsm_transition_to_shutdown_(); + if (clear_queue) { + this->clear_queued_valves(); + this->repeat_count_ = 0; + } +} + +void Sprinkler::pause() { + if (this->paused_valve_.has_value() || !this->active_req_.has_request()) { + 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->shutdown(false); + ESP_LOGD(TAG, "Paused valve %u with %u seconds remaining", this->paused_valve_.value_or(0), + this->resume_duration_.value_or(0)); +} + +void Sprinkler::resume() { + if (this->paused_valve_.has_value() && (this->resume_duration_.has_value())) { + ESP_LOGD(TAG, "Resuming valve %u with %u seconds remaining", this->paused_valve_.value_or(0), + this->resume_duration_.value_or(0)); + this->fsm_request_(this->paused_valve_.value(), this->resume_duration_.value()); + this->reset_resume_(); + } else { + ESP_LOGD(TAG, "No valve to resume!"); + } +} + +void Sprinkler::resume_or_start_full_cycle() { + if (this->paused_valve_.has_value() && (this->resume_duration_.has_value())) { + this->resume(); + } else { + this->start_full_cycle(); + } +} + +const char *Sprinkler::valve_name(const size_t valve_number) { + if (this->is_a_valid_valve(valve_number)) { + return this->valve_[valve_number].controller_switch->get_name().c_str(); + } + return nullptr; +} + +optional Sprinkler::active_valve() { return this->active_req_.valve_as_opt(); } +optional Sprinkler::paused_valve() { return this->paused_valve_; } + +optional Sprinkler::queued_valve() { + if (!this->queued_valves_.empty()) { + return this->queued_valves_.back().valve_number; + } + return nullopt; +} + +optional Sprinkler::manual_valve() { return this->manual_valve_; } + +size_t Sprinkler::number_of_valves() { return this->valve_.size(); } + +bool Sprinkler::is_a_valid_valve(const size_t valve_number) { + return ((valve_number >= 0) && (valve_number < this->number_of_valves())); +} + +bool Sprinkler::pump_in_use(SprinklerSwitch *pump_switch) { + if (pump_switch == nullptr) { + return false; // we can't do anything if there's nothing to check + } + // a pump must be considered "in use" if a (distribution) valve it supplies is active. this means: + // - at least one SprinklerValveOperator: + // - has a valve loaded that depends on this pump + // - is in a state that depends on the pump: (ACTIVE and _possibly_ STARTING/STOPPING) + // - if NO SprinklerValveOperator is active but there is a run request pending (active_req_.has_request()) and the + // controller state is STARTING, valve open delay is configured but NOT pump_switch_off_during_valve_open_delay_ + for (auto &vo : this->valve_op_) { // first, check if any SprinklerValveOperator has a valve dependent on this pump + if ((vo.state() != BYPASS) && (vo.pump_switch() != nullptr)) { + // the SprinklerValveOperator is configured with a pump; now check if it is the pump of interest + 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 + 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_)) { + return true; + } + } + } + } // if we end up here, no SprinklerValveOperator was in a "give-away" state indicating that the pump is in use... + if (!this->valve_overlap_ && !this->pump_switch_off_during_valve_open_delay_ && this->switching_delay_.has_value() && + this->active_req_.has_request() && (this->state_ != STOPPING)) { + // ...the controller is configured to keep the pump on during a valve open delay, so just return + // whether or not the next valve shares the same pump + return (pump_switch->off_switch() == this->valve_pump_switch(this->active_req_.valve())->off_switch()) && + (pump_switch->on_switch() == this->valve_pump_switch(this->active_req_.valve())->on_switch()); + } + return false; +} + +void Sprinkler::set_pump_state(SprinklerSwitch *pump_switch, bool state) { + if (pump_switch == nullptr) { + return; // we can't do anything if there's nothing to check + } + + bool hold_pump_on = false; + + for (auto &controller : this->other_controllers_) { // check if the pump is in use by another controller + if (controller != this) { // dummy check + if (controller->pump_in_use(pump_switch)) { + hold_pump_on = true; // if another controller says it's using this pump, keep it on + // at this point we know if there exists another SprinklerSwitch that is "on" with its + // off_switch_ and on_switch_ pointers pointing to the same pair of switch objects + } + } + } + if (hold_pump_on) { + // at this point we know if there exists another SprinklerSwitch that is "on" with its + // off_switch_ and on_switch_ pointers pointing to the same pair of switch objects... + pump_switch->sync_valve_state(true); // ...so ensure our state is consistent + ESP_LOGD(TAG, "Leaving pump on because another controller instance is using it"); + } + + if (state) { // ...and now we can set the new state of the switch + pump_switch->turn_on(); + } else if (!hold_pump_on && !this->pump_in_use(pump_switch)) { + pump_switch->turn_off(); + } else if (hold_pump_on) { // we must assume the other controller will switch off the pump when done... + pump_switch->sync_valve_state(false); // ...this only impacts latching valves + } +} + +optional Sprinkler::time_remaining() { + 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(); + } + } + for (auto &vo : this->valve_op_) { // ...else return the value from the first non-IDLE SprinklerValveOperator + if (vo.state() != IDLE) { + return vo.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; + } + return nullptr; +} + +SprinklerControllerSwitch *Sprinkler::enable_switch(size_t valve_number) { + if (this->is_a_valid_valve(valve_number)) { + return this->valve_[valve_number].enable_switch; + } + return nullptr; +} + +SprinklerSwitch *Sprinkler::valve_switch(const size_t valve_number) { + if (this->is_a_valid_valve(valve_number)) { + return &this->valve_[valve_number].valve_switch; + } + return nullptr; +} + +SprinklerSwitch *Sprinkler::valve_pump_switch(const size_t valve_number) { + if (this->is_a_valid_valve(valve_number) && this->valve_[valve_number].pump_switch_index.has_value()) { + return &this->pump_[this->valve_[valve_number].pump_switch_index.value()]; + } + return nullptr; +} + +SprinklerSwitch *Sprinkler::valve_pump_switch_by_pump_index(size_t pump_index) { + if (pump_index < this->pump_.size()) { + return &this->pump_[pump_index]; + } + return nullptr; +} + +uint32_t Sprinkler::hash_base() { return 3129891955UL; } + +bool Sprinkler::valve_is_enabled_(const size_t valve_number) { + if (this->is_a_valid_valve(valve_number)) { + if (this->valve_[valve_number].enable_switch != nullptr) { + return this->valve_[valve_number].enable_switch->state; + } else { + return true; + } + } + return false; +} + +void Sprinkler::mark_valve_cycle_complete_(const size_t valve_number) { + if (this->is_a_valid_valve(valve_number)) { + ESP_LOGD(TAG, "Marking valve %u complete", valve_number); + this->valve_[valve_number].valve_cycle_complete = true; + } +} + +bool Sprinkler::valve_cycle_complete_(const size_t valve_number) { + if (this->is_a_valid_valve(valve_number)) { + return this->valve_[valve_number].valve_cycle_complete; + } + 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; + + return 0; +} + +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; + + return this->number_of_valves() - 1; +} + +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); + } + } + return this->next_enabled_incomplete_valve_number_(first_valve); +} + +void Sprinkler::load_next_valve_run_request_(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())); + } + 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); + if (this->queued_valves_.back().run_duration) { + this->next_req_.set_run_duration(this->queued_valves_.back().run_duration); + } else { + this->next_req_.set_run_duration(this->valve_run_duration_adjusted(this->queued_valves_.back().valve_number)); + } + 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)); + 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))); + } + } + } +} + +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)) + return true; + } + return false; +} + +void Sprinkler::start_valve_(SprinklerValveRunRequest *req) { + if (!req->has_request()) { + return; // we can't do anything if the request contains nothing + } + if (!this->is_a_valid_valve(req->valve())) { + return; // we can't do anything if the valve number isn't valid + } + 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); + req->set_valve_operator(&vo); + vo.set_controller(this); + vo.set_valve(&this->valve_[req->valve()]); + vo.set_run_duration(run_duration); + vo.set_start_delay(this->start_delay_, this->start_delay_is_valve_delay_); + vo.set_stop_delay(this->stop_delay_, this->stop_delay_is_valve_delay_); + vo.start(); + return; + } + } +} + +void Sprinkler::all_valves_off_(const bool include_pump) { + for (size_t valve_index = 0; valve_index < this->number_of_valves(); valve_index++) { + if (this->valve_[valve_index].valve_switch.state()) { + this->valve_[valve_index].valve_switch.turn_off(); + } + if (include_pump) { + this->set_pump_state(this->valve_pump_switch(valve_index), false); + } + } + ESP_LOGD(TAG, "All valves stopped%s", include_pump ? ", including pumps" : ""); +} + +void Sprinkler::prep_full_cycle_() { + if (this->auto_adv_sw_ != nullptr) { + if (!this->auto_adv_sw_->state) { + this->auto_adv_sw_->publish_state(true); + } + } + if (!this->any_valve_is_enabled_()) { + for (auto &valve : this->valve_) { + if (valve.enable_switch != nullptr) { + valve.enable_switch->publish_state(true); + } + } + } + this->reset_cycle_states_(); +} + +void Sprinkler::reset_cycle_states_() { + for (auto &valve : this->valve_) { + valve.valve_cycle_complete = false; + } +} + +void Sprinkler::reset_resume_() { + this->paused_valve_.reset(); + this->resume_duration_.reset(); +} + +void Sprinkler::fsm_request_(size_t requested_valve, uint32_t requested_run_duration) { + this->next_req_.set_valve(requested_valve); + this->next_req_.set_run_duration(requested_run_duration); + // if state is IDLE or ACTIVE, call fsm_transition_() to start it immediately; + // otherwise, fsm_transition() will pick up next_req_ at the next appropriate transition + this->fsm_kick_(); +} + +void Sprinkler::fsm_kick_() { + if ((this->state_ == IDLE) || (this->state_ == ACTIVE)) { + this->fsm_transition_(); + } +} + +void Sprinkler::fsm_transition_() { + ESP_LOGVV(TAG, "fsm_transition_ called; state is %s", this->state_as_str_(this->state_).c_str()); + switch (this->state_) { + case IDLE: // the system was off -> start it up + // advances to ACTIVE + this->fsm_transition_from_shutdown_(); + break; + + case ACTIVE: + // advances to STOPPING or ACTIVE (again) + this->fsm_transition_from_valve_run_(); + break; + + case STARTING: { + // follows valve open delay interval + 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()) { + // another valve has been requested, so restart the timer so we pick it up quickly + this->set_timer_duration_(sprinkler::TIMER_SM, this->manual_selection_delay_.value_or(1)); + this->start_timer_(sprinkler::TIMER_SM); + } + break; + } + + case STOPPING: + // stop_delay_ has elapsed so just shut everything off + this->active_req_.reset(); + this->manual_valve_.reset(); + this->all_valves_off_(true); + this->state_ = IDLE; + break; + + default: + break; + } + if (this->next_req_.has_request() && (this->state_ == IDLE)) { + // another valve has been requested, so restart the timer so we pick it up quickly + this->set_timer_duration_(sprinkler::TIMER_SM, this->manual_selection_delay_.value_or(1)); + this->start_timer_(sprinkler::TIMER_SM); + } + ESP_LOGVV(TAG, "fsm_transition_ complete; new state is %s", this->state_as_str_(this->state_).c_str()); +} + +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; +} + +void Sprinkler::fsm_transition_from_valve_run_() { + if (!this->active_req_.has_request()) { // dummy check... + this->fsm_transition_to_shutdown_(); + return; + } + + 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()); + } else { + ESP_LOGD(TAG, "Valve cycle interrupted - NOT flagging valve as complete and stopping current valve"); + for (auto &vo : this->valve_op_) { + vo.stop(); + } + } + + this->load_next_valve_run_request_(this->active_req_.valve()); + + if (this->next_req_.has_request()) { // there is another valve to run... + bool same_pump = + 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_run_duration(this->next_req_.run_duration()); + this->next_req_.reset(); + + // this->state_ = ACTIVE; // state isn't changing + if (this->valve_overlap_ || !this->switching_delay_.has_value()) { + 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_); + } else { + this->set_timer_duration_( + sprinkler::TIMER_SM, + this->switching_delay_.value() * 2 + + (this->pump_switch_off_during_valve_open_delay_ && same_pump ? this->stop_delay_ : 0)); + this->start_timer_(sprinkler::TIMER_SM); + this->state_ = STARTING; + } + } else { // there is NOT another valve to run... + this->fsm_transition_to_shutdown_(); + } +} + +void Sprinkler::fsm_transition_to_shutdown_() { + this->state_ = STOPPING; + this->set_timer_duration_(sprinkler::TIMER_SM, + this->start_delay_ + this->stop_delay_ + this->switching_delay_.value_or(0) + 1); + this->start_timer_(sprinkler::TIMER_SM); +} + +std::string Sprinkler::state_as_str_(SprinklerState state) { + switch (state) { + case IDLE: + return "IDLE"; + + case STARTING: + return "STARTING"; + + case ACTIVE: + return "ACTIVE"; + + case STOPPING: + return "STOPPING"; + + case BYPASS: + return "BYPASS"; + + default: + return "UNKNOWN"; + } +} + +void Sprinkler::start_timer_(const SprinklerTimerIndex timer_index) { + if (this->timer_duration_(timer_index) > 0) { + this->set_timeout(this->timer_[timer_index].name, this->timer_duration_(timer_index), + this->timer_cbf_(timer_index)); + this->timer_[timer_index].start_time = millis(); + this->timer_[timer_index].active = true; + } + ESP_LOGVV(TAG, "Timer %u started for %u sec", static_cast(timer_index), + this->timer_duration_(timer_index) / 1000); +} + +bool Sprinkler::cancel_timer_(const SprinklerTimerIndex timer_index) { + this->timer_[timer_index].active = false; + return this->cancel_timeout(this->timer_[timer_index].name); +} + +bool Sprinkler::timer_active_(const SprinklerTimerIndex timer_index) { return this->timer_[timer_index].active; } + +void Sprinkler::set_timer_duration_(const SprinklerTimerIndex timer_index, const uint32_t time) { + this->timer_[timer_index].time = 1000 * time; +} + +uint32_t Sprinkler::timer_duration_(const SprinklerTimerIndex timer_index) { return this->timer_[timer_index].time; } + +std::function Sprinkler::timer_cbf_(const SprinklerTimerIndex timer_index) { + return this->timer_[timer_index].func; +} + +void Sprinkler::valve_selection_callback_() { + this->timer_[sprinkler::TIMER_VALVE_SELECTION].active = false; + ESP_LOGVV(TAG, "Valve selection timer expired"); + if (this->manual_valve_.has_value()) { + this->fsm_request_(this->manual_valve_.value()); + this->manual_valve_.reset(); + } +} + +void Sprinkler::sm_timer_callback_() { + this->timer_[sprinkler::TIMER_SM].active = false; + ESP_LOGVV(TAG, "State machine timer expired"); + this->fsm_transition_(); +} + +void Sprinkler::dump_config() { + ESP_LOGCONFIG(TAG, "Sprinkler Controller -- %s", this->name_.c_str()); + 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->start_delay_) { + if (this->start_delay_is_valve_delay_) { + ESP_LOGCONFIG(TAG, " Pump Start Valve Delay: %u seconds", this->start_delay_); + } else { + ESP_LOGCONFIG(TAG, " Pump Start Pump Delay: %u seconds", this->start_delay_); + } + } + if (this->stop_delay_) { + if (this->stop_delay_is_valve_delay_) { + ESP_LOGCONFIG(TAG, " Pump Stop Valve Delay: %u seconds", this->stop_delay_); + } else { + ESP_LOGCONFIG(TAG, " Pump Stop Pump Delay: %u seconds", this->stop_delay_); + } + } + if (this->switching_delay_.has_value()) { + if (this->valve_overlap_) { + ESP_LOGCONFIG(TAG, " Valve Overlap: %u seconds", this->switching_delay_.value_or(0)); + } else { + ESP_LOGCONFIG(TAG, " Valve Open Delay: %u seconds", this->switching_delay_.value_or(0)); + ESP_LOGCONFIG(TAG, " Pump Switch Off During Valve Open Delay: %s", + YESNO(this->pump_switch_off_during_valve_open_delay_)); + } + } + 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); + if (this->valve_[valve_number].valve_switch.pulse_duration()) { + ESP_LOGCONFIG(TAG, " Pulse Duration: %u milliseconds", + this->valve_[valve_number].valve_switch.pulse_duration()); + } + } + if (!this->pump_.empty()) { + ESP_LOGCONFIG(TAG, " Total number of pumps: %u", this->pump_.size()); + } + if (!this->valve_.empty()) { + ESP_LOGCONFIG(TAG, " Total number of valves: %u", this->valve_.size()); + } +} + +} // namespace sprinkler +} // namespace esphome diff --git a/esphome/components/sprinkler/sprinkler.h b/esphome/components/sprinkler/sprinkler.h new file mode 100644 index 0000000000..1243a844fa --- /dev/null +++ b/esphome/components/sprinkler/sprinkler.h @@ -0,0 +1,528 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/switch/switch.h" + +namespace esphome { +namespace sprinkler { + +enum SprinklerState : uint8_t { + // NOTE: these states are used by both SprinklerValveOperator and Sprinkler (the controller)! + IDLE, // system/valve is off + STARTING, // system/valve is starting/"half open" -- either pump or valve is on, but the remaining pump/valve is not + ACTIVE, // system/valve is running its cycle + STOPPING, // system/valve is stopping/"half open" -- either pump or valve is on, but the remaining pump/valve is not + BYPASS // used by SprinklerValveOperator to ignore the instance checking pump status +}; + +enum SprinklerTimerIndex : uint8_t { + TIMER_SM = 0, + TIMER_VALVE_SELECTION = 1, +}; + +class Sprinkler; // this component +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 +class SprinklerValveRunRequest; // tells the sprinkler controller what valve to run and for how long as well as what + // SprinklerValveOperator is handling it +template class StartSingleValveAction; +template class ShutdownAction; +template class ResumeOrStartAction; + +class SprinklerSwitch { + public: + SprinklerSwitch(); + SprinklerSwitch(switch_::Switch *sprinkler_switch); + SprinklerSwitch(switch_::Switch *off_switch, switch_::Switch *on_switch, uint32_t pulse_duration); + + bool is_latching_valve(); // returns true if configured as a latching valve + void loop(); // called as a part of loop(), used for latching valve pulses + uint32_t pulse_duration() { return this->pulse_duration_; } + bool state(); // returns the switch's current state + void set_off_switch(switch_::Switch *off_switch) { this->off_switch_ = off_switch; } + void set_on_switch(switch_::Switch *on_switch) { this->on_switch_ = on_switch; } + void set_pulse_duration(uint32_t pulse_duration) { this->pulse_duration_ = pulse_duration; } + void sync_valve_state( + bool latch_state); // syncs internal state to switch; if latching valve, sets state to latch_state + void turn_off(); // sets internal flag and actuates the switch + void turn_on(); // sets internal flag and actuates the switch + switch_::Switch *off_switch() { return this->off_switch_; } + switch_::Switch *on_switch() { return this->on_switch_; } + + protected: + bool state_{false}; + uint32_t pulse_duration_{0}; + uint64_t pinned_millis_{0}; + switch_::Switch *off_switch_{nullptr}; // only used for latching valves + switch_::Switch *on_switch_{nullptr}; // used for both latching and non-latching valves +}; + +struct SprinklerQueueItem { + size_t valve_number; + uint32_t run_duration; +}; + +struct SprinklerTimer { + const std::string name; + bool active; + uint32_t time; + uint32_t start_time; + std::function func; +}; + +struct SprinklerValve { + SprinklerControllerSwitch *controller_switch; + SprinklerControllerSwitch *enable_switch; + SprinklerSwitch valve_switch; + uint32_t run_duration; + optional pump_switch_index; + bool valve_cycle_complete; + std::unique_ptr> valve_shutdown_action; + std::unique_ptr> valve_resumeorstart_action; + std::unique_ptr> valve_turn_off_automation; + std::unique_ptr> valve_turn_on_automation; +}; + +class SprinklerControllerSwitch : public switch_::Switch, public Component { + public: + SprinklerControllerSwitch(); + + void setup() override; + 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 { + public: + SprinklerValveOperator(); + SprinklerValveOperator(SprinklerValve *valve, Sprinkler *controller); + void loop(); + void set_controller(Sprinkler *controller); + void set_valve(SprinklerValve *valve); + void set_run_duration(uint32_t run_duration); // set the desired run duration in seconds + void set_start_delay(uint32_t start_delay, bool start_delay_is_valve_delay); + void set_stop_delay(uint32_t stop_delay, bool stop_delay_is_valve_delay); + void start(); + void stop(); + uint32_t run_duration(); // returns the desired run duration in seconds + uint32_t time_remaining(); // returns seconds remaining (does not include stop_delay_) + SprinklerState state(); // returns the valve's state/status + SprinklerSwitch *pump_switch(); // returns this SprinklerValveOperator's pump's SprinklerSwitch + + protected: + void pump_off_(); + void pump_on_(); + void valve_off_(); + void valve_on_(); + void kill_(); + void run_(); + bool start_delay_is_valve_delay_{false}; + bool stop_delay_is_valve_delay_{false}; + uint32_t start_delay_{0}; + uint32_t stop_delay_{0}; + uint32_t run_duration_{0}; + uint64_t pinned_millis_{0}; + Sprinkler *controller_{nullptr}; + SprinklerValve *valve_{nullptr}; + SprinklerState state_{IDLE}; +}; + +class SprinklerValveRunRequest { + public: + SprinklerValveRunRequest(); + SprinklerValveRunRequest(size_t valve_number, uint32_t run_duration, SprinklerValveOperator *valve_op); + bool has_request(); + bool has_valve_operator(); + void set_run_duration(uint32_t run_duration); + void set_valve(size_t valve_number); + void set_valve_operator(SprinklerValveOperator *valve_op); + void reset(); + uint32_t run_duration(); + size_t valve(); + optional valve_as_opt(); + SprinklerValveOperator *valve_operator(); + + protected: + bool has_valve_{false}; + size_t valve_number_{0}; + uint32_t run_duration_{0}; + SprinklerValveOperator *valve_op_{nullptr}; +}; + +class Sprinkler : public Component, public EntityBase { + public: + Sprinkler(); + Sprinkler(const std::string &name); + + void setup() override; + void loop() override; + void dump_config() override; + + /// add a valve to the controller + void add_valve(SprinklerControllerSwitch *valve_sw, SprinklerControllerSwitch *enable_sw = nullptr); + + /// add another controller to the controller so it can check if pumps/main valves are in use + void add_controller(Sprinkler *other_controller); + + /// configure important controller switches + void set_controller_main_switch(SprinklerControllerSwitch *controller_switch); + 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); + + /// 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); + void configure_valve_switch_pulsed(size_t valve_number, switch_::Switch *valve_switch_off, + switch_::Switch *valve_switch_on, uint32_t pulse_duration, uint32_t run_duration); + + /// configure a valve's associated pump switch object + void configure_valve_pump_switch(size_t valve_number, switch_::Switch *pump_switch); + void configure_valve_pump_switch_pulsed(size_t valve_number, switch_::Switch *pump_switch_off, + switch_::Switch *pump_switch_on, uint32_t pulse_duration); + + /// value multiplied by configured run times -- used to extend or shorten the cycle + void set_multiplier(optional multiplier); + + /// set how long the pump should start after the valve (when the pump is starting) + void set_pump_start_delay(uint32_t start_delay); + + /// set how long the pump should stop after the valve (when the pump is starting) + void set_pump_stop_delay(uint32_t stop_delay); + + /// set how long the valve should start after the pump (when the pump is stopping) + void set_valve_start_delay(uint32_t start_delay); + + /// set how long the valve should stop after the pump (when the pump is stopping) + void set_valve_stop_delay(uint32_t stop_delay); + + /// if pump_switch_off_during_valve_open_delay is true, the controller will switch off the pump during the + /// valve_open_delay interval + void set_pump_switch_off_during_valve_open_delay(bool pump_switch_off_during_valve_open_delay); + + /// set how long the controller should wait to open/switch on the valve after it becomes active + void set_valve_open_delay(uint32_t valve_open_delay); + + /// set how long the controller should wait after opening a valve before closing the previous valve + void set_valve_overlap(uint32_t valve_overlap); + + /// set how long the controller should wait to activate a valve after next_valve() or previous_valve() is called + void set_manual_selection_delay(uint32_t manual_selection_delay); + + /// set how long the valve should remain on/open. run_duration is time in seconds + void set_valve_run_duration(optional valve_number, optional run_duration); + + /// if auto_advance is true, controller will iterate through all enabled valves + void set_auto_advance(bool auto_advance); + + /// set the number of times to repeat a full cycle + void set_repeat(optional repeat); + + /// if queue_enable is true, controller will iterate through valves in the queue + void set_queue_enable(bool queue_enable); + + /// if reverse is true, controller will iterate through all enabled valves in reverse (descending) order + void set_reverse(bool reverse); + + /// returns valve_number's run duration in seconds + uint32_t valve_run_duration(size_t valve_number); + + /// returns valve_number's run duration (in seconds) adjusted by multiplier_ + uint32_t valve_run_duration_adjusted(size_t valve_number); + + /// returns true if auto_advance is enabled + bool auto_advance(); + + /// returns the current value of the multiplier + float multiplier(); + + /// returns the number of times the controller is set to repeat cycles, if at all. check with 'has_value()' + optional repeat(); + + /// if a cycle is active, returns the number of times the controller has repeated the cycle. check with 'has_value()' + optional repeat_count(); + + /// returns true if the queue is enabled to run + bool queue_enabled(); + + /// returns true if reverse is enabled + bool reverse(); + + /// 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(); + + /// starts a full cycle of all enabled valves and enables auto_advance. + /// if no valves are enabled, all valves will be enabled. + void start_full_cycle(); + + /// activates a single valve and disables auto_advance. + void start_single_valve(optional valve_number); + + /// 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. + void queue_valve(optional valve_number, optional run_duration); + + /// clears/removes all valves from the queue + void clear_queued_valves(); + + /// advances to the next valve (numerically) + void next_valve(); + + /// advances to the previous valve (numerically) + void previous_valve(); + + /// turns off all valves, effectively shutting down the system. + void shutdown(bool clear_queue = false); + + /// same as shutdown(), but also stores active_valve() and time_remaining() allowing resume() to continue the cycle + void pause(); + + /// resumes a cycle that was suspended using pause() + void resume(); + + /// if a cycle was suspended using pause(), resumes it. otherwise calls start_full_cycle() + void resume_or_start_full_cycle(); + + /// 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 the number of the valve that is currently active, if any. check with 'has_value()' + optional active_valve(); + + /// returns the number of the valve that is paused, if any. check with 'has_value()' + optional paused_valve(); + + /// returns the number of the next valve in the queue, if any. check with 'has_value()' + optional queued_valve(); + + /// returns the number of the valve that is manually selected, if any. check with 'has_value()' + /// this is set by next_valve() and previous_valve() when manual_selection_delay_ > 0 + optional manual_valve(); + + /// returns the number of valves the controller is configured with + size_t number_of_valves(); + + /// returns true if valve number is valid + bool is_a_valid_valve(size_t valve_number); + + /// returns true if the pump the pointer points to is in use + bool pump_in_use(SprinklerSwitch *pump_switch); + + /// 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 a pointer to a valve's control switch object + SprinklerControllerSwitch *control_switch(size_t valve_number); + + /// returns a pointer to a valve's enable switch object + SprinklerControllerSwitch *enable_switch(size_t valve_number); + + /// returns a pointer to a valve's switch object + SprinklerSwitch *valve_switch(size_t valve_number); + + /// returns a pointer to a valve's pump switch object + SprinklerSwitch *valve_pump_switch(size_t valve_number); + + /// returns a pointer to a valve's pump switch object + SprinklerSwitch *valve_pump_switch_by_pump_index(size_t pump_index); + + protected: + uint32_t hash_base() override; + + /// returns true if valve number is enabled + bool valve_is_enabled_(size_t valve_number); + + /// marks a valve's cycle as complete + void mark_valve_cycle_complete_(size_t valve_number); + + /// 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 that should be activated in a full cycle. + /// if no valve is next (cycle is complete), returns no value (check with 'has_value()') + optional next_valve_number_in_cycle_(optional first_valve = nullopt); + + /// loads next_req_ with the next valve that should be activated, including its run duration. + /// if next_req_ already contains a request, nothing is done. after next_req_, + /// queued valves have priority, followed by enabled valves if auto-advance is enabled. + /// 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_(); + + /// loads an available SprinklerValveOperator (valve_op_) based on req and starts it (switches it on). + /// NOTE: if run_duration is zero, the valve's run_duration will be set based on the valve's configuration. + void start_valve_(SprinklerValveRunRequest *req); + + /// turns off/closes all valves, including pump if include_pump is true + void all_valves_off_(bool include_pump = false); + + /// prepares for a full cycle by verifying auto-advance is on as well as one or more valve enable switches. + void prep_full_cycle_(); + + /// resets the cycle state for all valves + void reset_cycle_states_(); + + /// resets resume state + void reset_resume_(); + + /// make a request of the state machine + void fsm_request_(size_t requested_valve, uint32_t requested_run_duration = 0); + + /// kicks the state machine to advance, starting it if it is not already active + void fsm_kick_(); + + /// advance controller state, advancing to target_valve if provided + void fsm_transition_(); + + /// starts up the system from IDLE state + void fsm_transition_from_shutdown_(); + + /// transitions from ACTIVE state to ACTIVE (as in, next valve) or to a SHUTDOWN or IDLE state + void fsm_transition_from_valve_run_(); + + /// starts up the system from IDLE state + void fsm_transition_to_shutdown_(); + + /// return the current FSM state as a string + std::string state_as_str_(SprinklerState state); + + /// Start/cancel/get status of valve timers + void start_timer_(SprinklerTimerIndex timer_index); + bool cancel_timer_(SprinklerTimerIndex timer_index); + /// returns true if the specified timer is active/running + bool timer_active_(SprinklerTimerIndex timer_index); + /// time is converted to milliseconds (ms) for set_timeout() + void set_timer_duration_(SprinklerTimerIndex timer_index, uint32_t time); + /// returns time in milliseconds (ms) + uint32_t timer_duration_(SprinklerTimerIndex timer_index); + std::function timer_cbf_(SprinklerTimerIndex timer_index); + + /// callback functions for timers + void valve_selection_callback_(); + void sm_timer_callback_(); + void pump_stop_delay_callback_(); + + /// Maximum allowed queue size + const uint8_t max_queue_size_{100}; + + /// Pump should be off during valve_open_delay interval + bool pump_switch_off_during_valve_open_delay_{false}; + + /// Sprinkler valve cycle should overlap + bool valve_overlap_{false}; + + /// Pump start/stop delay interval types + bool start_delay_is_valve_delay_{false}; + bool stop_delay_is_valve_delay_{false}; + + /// Pump start/stop delay intervals + uint32_t start_delay_{0}; + uint32_t stop_delay_{0}; + + /// Sprinkler controller state + SprinklerState state_{IDLE}; + + /// The valve run request that is currently active + SprinklerValveRunRequest active_req_; + + /// The number of the manually selected valve currently selected + optional manual_valve_; + + /// The number of the valve to resume from (if paused) + optional paused_valve_; + + /// The next run request for the controller to consume after active_req_ is complete + SprinklerValveRunRequest next_req_; + + /// Set the number of times to repeat a full cycle + optional target_repeats_; + + /// Set from time_remaining() when paused + optional resume_duration_; + + /// Manual switching delay + optional manual_selection_delay_; + + /// Valve switching delay + optional switching_delay_; + + /// Number of times the full cycle has been repeated + uint32_t repeat_count_{0}; + + /// Sprinkler valve run time multiplier value + float multiplier_{1.0}; + + /// Queue of valves to activate next, regardless of auto-advance + std::vector queued_valves_; + + /// Sprinkler valve pump objects + std::vector pump_; + + /// Sprinkler valve objects + std::vector valve_; + + /// Sprinkler valve operator objects + std::vector valve_op_{2}; + + /// Valve control timers + std::vector timer_{ + {this->name_ + "sm", false, 0, 0, std::bind(&Sprinkler::sm_timer_callback_, this)}, + {this->name_ + "vs", false, 0, 0, std::bind(&Sprinkler::valve_selection_callback_, this)}}; + + /// Other Sprinkler instances we should be aware of (used to check if pumps are in use) + std::vector other_controllers_; + + /// Switches we'll present to the front end + SprinklerControllerSwitch *auto_adv_sw_{nullptr}; + SprinklerControllerSwitch *controller_sw_{nullptr}; + SprinklerControllerSwitch *queue_enable_sw_{nullptr}; + SprinklerControllerSwitch *reverse_sw_{nullptr}; + + std::unique_ptr> sprinkler_shutdown_action_; + std::unique_ptr> sprinkler_resumeorstart_action_; + + std::unique_ptr> sprinkler_turn_off_automation_; + std::unique_ptr> sprinkler_turn_on_automation_; +}; + +} // namespace sprinkler +} // namespace esphome diff --git a/tests/test3.yaml b/tests/test3.yaml index 560eda2585..1a862b515e 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -1128,6 +1128,49 @@ climate: ki: 0.0 kd: 0.0 +sprinkler: + - id: yard_sprinkler_ctrlr + main_switch: "Yard Sprinklers" + auto_advance_switch: "Yard Sprinklers Auto Advance" + reverse_switch: "Yard Sprinklers Reverse" + pump_start_pump_delay: 2s + pump_stop_valve_delay: 4s + pump_switch_off_during_valve_open_delay: true + valve_open_delay: 5s + valves: + - valve_switch: "Yard Valve 0" + enable_switch: "Enable Yard Valve 0" + pump_switch_id: gpio_switch1 + run_duration: 10s + valve_switch_id: gpio_switch2 + - valve_switch: "Yard Valve 1" + enable_switch: "Enable Yard Valve 1" + pump_switch_id: gpio_switch1 + run_duration: 10s + valve_switch_id: gpio_switch2 + - valve_switch: "Yard Valve 2" + enable_switch: "Enable Yard Valve 2" + pump_switch_id: gpio_switch1 + run_duration: 10s + valve_switch_id: gpio_switch2 + - id: garden_sprinkler_ctrlr + main_switch: "Garden Sprinklers" + auto_advance_switch: "Garden Sprinklers Auto Advance" + reverse_switch: "Garden Sprinklers Reverse" + valve_overlap: 5s + valves: + - valve_switch: "Garden Valve 0" + enable_switch: "Enable Garden Valve 0" + pump_switch_id: gpio_switch1 + run_duration: 10s + valve_switch_id: gpio_switch2 + - valve_switch: "Garden Valve 1" + enable_switch: "Enable Garden Valve 1" + pump_switch_id: gpio_switch1 + run_duration: 10s + valve_switch_id: gpio_switch2 + + cover: - platform: endstop name: Endstop Cover