diff --git a/esphome/components/switch/__init__.py b/esphome/components/switch/__init__.py index a595d43445..4668a1458c 100644 --- a/esphome/components/switch/__init__.py +++ b/esphome/components/switch/__init__.py @@ -13,6 +13,7 @@ from esphome.const import ( CONF_ON_TURN_OFF, CONF_ON_TURN_ON, CONF_RESTORE_MODE, + CONF_STATE, CONF_TRIGGER_ID, CONF_WEB_SERVER, DEVICE_CLASS_EMPTY, @@ -48,6 +49,7 @@ RESTORE_MODES = { } +ControlAction = switch_ns.class_("ControlAction", automation.Action) ToggleAction = switch_ns.class_("ToggleAction", automation.Action) TurnOffAction = switch_ns.class_("TurnOffAction", automation.Action) TurnOnAction = switch_ns.class_("TurnOnAction", automation.Action) @@ -177,6 +179,23 @@ SWITCH_ACTION_SCHEMA = maybe_simple_id( cv.Required(CONF_ID): cv.use_id(Switch), } ) +SWITCH_CONTROL_ACTION_SCHEMA = automation.maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(Switch), + cv.Required(CONF_STATE): cv.templatable(cv.boolean), + } +) + + +@automation.register_action( + "switch.control", ControlAction, SWITCH_CONTROL_ACTION_SCHEMA +) +async def switch_control_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_STATE], args, bool) + cg.add(var.set_state(template_)) + return var @automation.register_action("switch.toggle", ToggleAction, SWITCH_ACTION_SCHEMA) diff --git a/esphome/components/switch/automation.h b/esphome/components/switch/automation.h index 579daf4d24..66818a80be 100644 --- a/esphome/components/switch/automation.h +++ b/esphome/components/switch/automation.h @@ -37,6 +37,23 @@ template class ToggleAction : public Action { Switch *switch_; }; +template class ControlAction : public Action { + public: + explicit ControlAction(Switch *a_switch) : switch_(a_switch) {} + + TEMPLATABLE_VALUE(bool, state) + + void play(Ts... x) override { + auto state = this->state_.optional_value(x...); + if (state.has_value()) { + this->switch_->control(*state); + } + } + + protected: + Switch *switch_; +}; + template class SwitchCondition : public Condition { public: SwitchCondition(Switch *parent, bool state) : parent_(parent), state_(state) {} diff --git a/esphome/components/switch/switch.cpp b/esphome/components/switch/switch.cpp index c204895755..13c12c1213 100644 --- a/esphome/components/switch/switch.cpp +++ b/esphome/components/switch/switch.cpp @@ -8,6 +8,14 @@ static const char *const TAG = "switch"; Switch::Switch() : state(false) {} +void Switch::control(bool target_state) { + ESP_LOGV(TAG, "'%s' Control: %s", this->get_name().c_str(), ONOFF(target_state)); + if (target_state) { + this->turn_on(); + } else { + this->turn_off(); + } +} void Switch::turn_on() { ESP_LOGD(TAG, "'%s' Turning ON.", this->get_name().c_str()); this->write_state(!this->inverted_); diff --git a/esphome/components/switch/switch.h b/esphome/components/switch/switch.h index b999296564..6371e35292 100644 --- a/esphome/components/switch/switch.h +++ b/esphome/components/switch/switch.h @@ -55,6 +55,14 @@ class Switch : public EntityBase, public EntityBase_DeviceClass { /// The current reported state of the binary sensor. bool state; + /** Control this switch using a boolean state value. + * + * This method provides a unified interface for setting the switch state based on a boolean parameter. + * It automatically calls turn_on() when state is true or turn_off() when state is false. + * + * @param target_state The desired state: true to turn the switch ON, false to turn it OFF. + */ + void control(bool target_state); /** Turn this switch on. This is called by the front-end. * * For implementing switches, please override write_state. diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index 46f09336bb..9db389c39a 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -324,14 +324,13 @@ class EsphomePortCommandWebSocket(EsphomeCommandWebSocket): configuration = json_message["configuration"] config_file = settings.rel_path(configuration) port = json_message["port"] - addresses: list[str] = [port] + addresses: list[str] = [] if ( port == "OTA" # pylint: disable=too-many-boolean-expressions and (entry := entries.get(config_file)) and entry.loaded_integrations and "api" in entry.loaded_integrations ): - addresses = [] # First priority: entry.address AKA use_address if ( (use_address := entry.address) @@ -359,6 +358,13 @@ class EsphomePortCommandWebSocket(EsphomeCommandWebSocket): # since MQTT logging will not work otherwise addresses.extend(sort_ip_addresses(new_addresses)) + if not addresses: + # If no address was found, use the port directly + # as otherwise they will get the chooser which + # does not work with the dashboard as there is no + # interactive way to get keyboard input + addresses = [port] + device_args: list[str] = [ arg for address in addresses for arg in ("--device", address) ] diff --git a/tests/components/switch/common.yaml b/tests/components/switch/common.yaml index 8d6972f91b..b69e36a1c0 100644 --- a/tests/components/switch/common.yaml +++ b/tests/components/switch/common.yaml @@ -9,3 +9,11 @@ switch: name: "Template Switch" id: the_switch optimistic: true + +esphome: + on_boot: + - switch.turn_on: the_switch + - switch.turn_off: the_switch + - switch.control: + id: the_switch + state: !lambda return (1 > 2);