diff --git a/esphome/components/binary_sensor/binary_sensor.h b/esphome/components/binary_sensor/binary_sensor.h
index 301a472810..57cae9e2f5 100644
--- a/esphome/components/binary_sensor/binary_sensor.h
+++ b/esphome/components/binary_sensor/binary_sensor.h
@@ -58,7 +58,7 @@ class BinarySensor : public EntityBase, public EntityBase_DeviceClass {
   void publish_initial_state(bool state);
 
   /// The current reported state of the binary sensor.
-  bool state;
+  bool state{false};
 
   void add_filter(Filter *filter);
   void add_filters(const std::vector<Filter *> &filters);
diff --git a/esphome/components/display_menu_base/__init__.py b/esphome/components/display_menu_base/__init__.py
index 8ae9cbc2a4..f9c0424104 100644
--- a/esphome/components/display_menu_base/__init__.py
+++ b/esphome/components/display_menu_base/__init__.py
@@ -68,8 +68,6 @@ IsActiveCondition = display_menu_base_ns.class_(
     "IsActiveCondition", automation.Condition
 )
 
-MULTI_CONF = True
-
 MenuItemType = display_menu_base_ns.enum("MenuItemType")
 
 MENU_ITEM_TYPES = {
diff --git a/esphome/components/graphical_display_menu/__init__.py b/esphome/components/graphical_display_menu/__init__.py
index f4d59b22b8..56b720e75c 100644
--- a/esphome/components/graphical_display_menu/__init__.py
+++ b/esphome/components/graphical_display_menu/__init__.py
@@ -36,6 +36,8 @@ CODEOWNERS = ["@MrMDavidson"]
 
 AUTO_LOAD = ["display_menu_base"]
 
+MULTI_CONF = True
+
 CONFIG_SCHEMA = DISPLAY_MENU_BASE_SCHEMA.extend(
     cv.Schema(
         {
diff --git a/esphome/components/matrix_keypad/binary_sensor/matrix_keypad_binary_sensor.h b/esphome/components/matrix_keypad/binary_sensor/matrix_keypad_binary_sensor.h
index d8a217f55e..2c1ce96f0a 100644
--- a/esphome/components/matrix_keypad/binary_sensor/matrix_keypad_binary_sensor.h
+++ b/esphome/components/matrix_keypad/binary_sensor/matrix_keypad_binary_sensor.h
@@ -6,7 +6,7 @@
 namespace esphome {
 namespace matrix_keypad {
 
-class MatrixKeypadBinarySensor : public MatrixKeypadListener, public binary_sensor::BinarySensor {
+class MatrixKeypadBinarySensor : public MatrixKeypadListener, public binary_sensor::BinarySensorInitiallyOff {
  public:
   MatrixKeypadBinarySensor(uint8_t key) : has_key_(true), key_(key){};
   MatrixKeypadBinarySensor(const char *key) : has_key_(true), key_((uint8_t) key[0]){};
diff --git a/esphome/components/modbus_controller/__init__.py b/esphome/components/modbus_controller/__init__.py
index 5c407d6fff..2a08075831 100644
--- a/esphome/components/modbus_controller/__init__.py
+++ b/esphome/components/modbus_controller/__init__.py
@@ -163,7 +163,7 @@ CONFIG_SCHEMA = cv.All(
             ),
             cv.Optional(CONF_ON_OFFLINE): automation.validate_automation(
                 {
-                    cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ModbusOnlineTrigger),
+                    cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ModbusOfflineTrigger),
                 }
             ),
         }
diff --git a/esphome/components/status/binary_sensor.py b/esphome/components/status/binary_sensor.py
index 1f2b7c9d18..adc342ed4d 100644
--- a/esphome/components/status/binary_sensor.py
+++ b/esphome/components/status/binary_sensor.py
@@ -6,6 +6,8 @@ from esphome.const import (
     ENTITY_CATEGORY_DIAGNOSTIC,
 )
 
+DEPENDENCIES = ["network"]
+
 status_ns = cg.esphome_ns.namespace("status")
 StatusBinarySensor = status_ns.class_(
     "StatusBinarySensor", binary_sensor.BinarySensor, cg.Component
diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py
index ea03cc16d1..ad1a4f5262 100644
--- a/esphome/components/wifi/__init__.py
+++ b/esphome/components/wifi/__init__.py
@@ -27,6 +27,7 @@ from esphome.const import (
     CONF_NETWORKS,
     CONF_ON_CONNECT,
     CONF_ON_DISCONNECT,
+    CONF_ON_ERROR,
     CONF_PASSWORD,
     CONF_POWER_SAVE_MODE,
     CONF_PRIORITY,
@@ -34,6 +35,7 @@ from esphome.const import (
     CONF_SSID,
     CONF_STATIC_IP,
     CONF_SUBNET,
+    CONF_TIMEOUT,
     CONF_TTLS_PHASE_2,
     CONF_USE_ADDRESS,
     CONF_USERNAME,
@@ -46,6 +48,7 @@ from . import wpa2_eap
 AUTO_LOAD = ["network"]
 
 NO_WIFI_VARIANTS = [const.VARIANT_ESP32H2]
+CONF_SAVE = "save"
 
 wifi_ns = cg.esphome_ns.namespace("wifi")
 EAPAuth = wifi_ns.struct("EAPAuth")
@@ -63,6 +66,9 @@ WiFiConnectedCondition = wifi_ns.class_("WiFiConnectedCondition", Condition)
 WiFiEnabledCondition = wifi_ns.class_("WiFiEnabledCondition", Condition)
 WiFiEnableAction = wifi_ns.class_("WiFiEnableAction", automation.Action)
 WiFiDisableAction = wifi_ns.class_("WiFiDisableAction", automation.Action)
+WiFiConfigureAction = wifi_ns.class_(
+    "WiFiConfigureAction", automation.Action, cg.Component
+)
 
 
 def validate_password(value):
@@ -483,3 +489,39 @@ async def wifi_enable_to_code(config, action_id, template_arg, args):
 @automation.register_action("wifi.disable", WiFiDisableAction, cv.Schema({}))
 async def wifi_disable_to_code(config, action_id, template_arg, args):
     return cg.new_Pvariable(action_id, template_arg)
+
+
+@automation.register_action(
+    "wifi.configure",
+    WiFiConfigureAction,
+    cv.Schema(
+        {
+            cv.Required(CONF_SSID): cv.templatable(cv.ssid),
+            cv.Required(CONF_PASSWORD): cv.templatable(validate_password),
+            cv.Optional(CONF_SAVE, default=True): cv.templatable(cv.boolean),
+            cv.Optional(CONF_TIMEOUT, default="30000ms"): cv.templatable(
+                cv.positive_time_period_milliseconds
+            ),
+            cv.Optional(CONF_ON_CONNECT): automation.validate_automation(single=True),
+            cv.Optional(CONF_ON_ERROR): automation.validate_automation(single=True),
+        }
+    ),
+)
+async def wifi_set_sta_to_code(config, action_id, template_arg, args):
+    var = cg.new_Pvariable(action_id, template_arg)
+    ssid = await cg.templatable(config[CONF_SSID], args, cg.std_string)
+    password = await cg.templatable(config[CONF_PASSWORD], args, cg.std_string)
+    save = await cg.templatable(config[CONF_SAVE], args, cg.bool_)
+    timeout = await cg.templatable(config.get(CONF_TIMEOUT), args, cg.uint32)
+    cg.add(var.set_ssid(ssid))
+    cg.add(var.set_password(password))
+    cg.add(var.set_save(save))
+    cg.add(var.set_connection_timeout(timeout))
+    if on_connect_config := config.get(CONF_ON_CONNECT):
+        await automation.build_automation(
+            var.get_connect_trigger(), [], on_connect_config
+        )
+    if on_error_config := config.get(CONF_ON_ERROR):
+        await automation.build_automation(var.get_error_trigger(), [], on_error_config)
+    await cg.register_component(var, config)
+    return var
diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h
index 5995f72e0b..abedfab3a6 100644
--- a/esphome/components/wifi/wifi_component.h
+++ b/esphome/components/wifi/wifi_component.h
@@ -209,6 +209,7 @@ class WiFiComponent : public Component {
   WiFiComponent();
 
   void set_sta(const WiFiAP &ap);
+  WiFiAP get_sta() { return this->selected_ap_; }
   void add_sta(const WiFiAP &ap);
   void clear_sta();
 
@@ -443,6 +444,84 @@ template<typename... Ts> class WiFiDisableAction : public Action<Ts...> {
   void play(Ts... x) override { global_wifi_component->disable(); }
 };
 
+template<typename... Ts> class WiFiConfigureAction : public Action<Ts...>, public Component {
+ public:
+  TEMPLATABLE_VALUE(std::string, ssid)
+  TEMPLATABLE_VALUE(std::string, password)
+  TEMPLATABLE_VALUE(bool, save)
+  TEMPLATABLE_VALUE(uint32_t, connection_timeout)
+
+  void play(Ts... x) override {
+    auto ssid = this->ssid_.value(x...);
+    auto password = this->password_.value(x...);
+    // Avoid multiple calls
+    if (this->connecting_)
+      return;
+    // If already connected to the same AP, do nothing
+    if (global_wifi_component->wifi_ssid() == ssid) {
+      // Callback to notify the user that the connection was successful
+      this->connect_trigger_->trigger();
+      return;
+    }
+    // Create a new WiFiAP object with the new SSID and password
+    this->new_sta_.set_ssid(ssid);
+    this->new_sta_.set_password(password);
+    // Save the current STA
+    this->old_sta_ = global_wifi_component->get_sta();
+    // Disable WiFi
+    global_wifi_component->disable();
+    // Set the state to connecting
+    this->connecting_ = true;
+    // Store the new STA so once the WiFi is enabled, it will connect to it
+    // This is necessary because the WiFiComponent will raise an error and fallback to the saved STA
+    // if trying to connect to a new STA while already connected to another one
+    if (this->save_.value(x...)) {
+      global_wifi_component->save_wifi_sta(new_sta_.get_ssid(), new_sta_.get_password());
+    } else {
+      global_wifi_component->set_sta(new_sta_);
+    }
+    // Enable WiFi
+    global_wifi_component->enable();
+    // Set timeout for the connection
+    this->set_timeout("wifi-connect-timeout", this->connection_timeout_.value(x...), [this]() {
+      this->connecting_ = false;
+      // If the timeout is reached, stop connecting and revert to the old AP
+      global_wifi_component->disable();
+      global_wifi_component->save_wifi_sta(old_sta_.get_ssid(), old_sta_.get_password());
+      global_wifi_component->enable();
+      // Callback to notify the user that the connection failed
+      this->error_trigger_->trigger();
+    });
+  }
+
+  Trigger<> *get_connect_trigger() const { return this->connect_trigger_; }
+  Trigger<> *get_error_trigger() const { return this->error_trigger_; }
+
+  void loop() override {
+    if (!this->connecting_)
+      return;
+    if (global_wifi_component->is_connected()) {
+      // The WiFi is connected, stop the timeout and reset the connecting flag
+      this->cancel_timeout("wifi-connect-timeout");
+      this->connecting_ = false;
+      if (global_wifi_component->wifi_ssid() == this->new_sta_.get_ssid()) {
+        // Callback to notify the user that the connection was successful
+        this->connect_trigger_->trigger();
+      } else {
+        // Callback to notify the user that the connection failed
+        this->error_trigger_->trigger();
+      }
+    }
+  }
+
+ protected:
+  bool connecting_{false};
+  WiFiAP new_sta_;
+  WiFiAP old_sta_;
+  Trigger<> *connect_trigger_{new Trigger<>()};
+  Trigger<> *error_trigger_{new Trigger<>()};
+};
+
 }  // namespace wifi
 }  // namespace esphome
 #endif
diff --git a/tests/components/wifi/common.yaml b/tests/components/wifi/common.yaml
index 003f6347be..343d44b177 100644
--- a/tests/components/wifi/common.yaml
+++ b/tests/components/wifi/common.yaml
@@ -3,6 +3,13 @@ esphome:
     then:
       - wifi.disable
       - wifi.enable
+      - wifi.configure:
+          ssid: MySSID
+          password: password1
+          on_connect:
+            - logger.log: "Connected to WiFi!"
+          on_error:
+            - logger.log: "Failed to connect to WiFi!"
 
 wifi:
   ssid: MySSID