Switch
The switch component is one of ESPHome’s core entity types. A switch is a simple
two-state (ON/OFF) entity that the user can control from the front-end (Home Assistant, the web server, MQTT, etc.). It
is conceptually a binary sensor that can also be written to: in addition to
reporting its current state, it accepts turn-on/turn-off/toggle commands and drives some piece of hardware in response.
Like the other entity types, switch is a platform base. Individual components (for example gpio, template or a
relay board) register a switch and implement the logic that actually turns the hardware on and off.
We have an example, minimal switch component which is a good starting point.
Python
Section titled “Python”A switch platform lives in a switch.py file (or a switch/__init__.py package) inside your component’s directory. This
file name tells ESPHome that the component provides a switch platform, allowing the user to configure it under the
switch: block:
switch: - platform: my_component name: "My Switch"The typical imports and class declaration look like this:
import esphome.codegen as cgimport esphome.config_validation as cvfrom esphome.components import switch
my_switch_ns = cg.esphome_ns.namespace("my_switch")MySwitch = my_switch_ns.class_("MySwitch", switch.Switch, cg.Component)Note that MySwitch inherits from both switch.Switch and a component base (cg.Component here).
Configuration schema
Section titled “Configuration schema”Use the switch.switch_schema() helper. It returns a schema pre-populated with all of the options common to every
switch - name, id, icon, inverted, restore_mode, device_class, entity_category, the on_turn_on /
on_turn_off automations, and so on - and lets you pass defaults for your device:
CONFIG_SCHEMA = switch.switch_schema(MySwitch).extend(cv.COMPONENT_SCHEMA)Passing your class (MySwitch) as the first argument tells the helper which class to declare the ID for, so you do not
need a separate cv.GenerateID().
Code generation
Section titled “Code generation”In to_code, use the switch.new_switch() helper. It calls cg.new_Pvariable() for you and generates all the code to
apply the common switch options from the configuration:
async def to_code(config): var = await switch.new_switch(config) await cg.register_component(var, config)If your component owns a switch as a child (rather than being a switch), use await switch.register_switch(var, config)
instead, having declared the ID yourself.
The C++ class inherits from switch_::Switch alongside its component base. Note the trailing underscore in the
namespace: switch is a reserved C++ keyword, so the namespace is spelled switch_.
#include "esphome/core/component.h"#include "esphome/components/switch/switch.h"
namespace esphome::my_switch {
class MySwitch : public switch_::Switch, public Component { public: void dump_config() override;
protected: void write_state(bool state) override;};
} // namespace esphome::my_switchWriting state to hardware
Section titled “Writing state to hardware”The one method you must implement is write_state(). It is called by the front-end when the user (or an automation)
turns the switch on or off. Your implementation drives the hardware and then calls publish_state() to acknowledge that
the new state was applied:
void MySwitch::write_state(bool state) { // Drive the hardware here (set a GPIO, send a command, etc.) this->write_hardware_(state);
// Acknowledge the new state back to the front-end. this->publish_state(state);}A few important details:
- The
stateargument already has the user’sinverted:option applied, so you can write it to the hardware directly. - You should call
publish_state()yourself once the hardware has been driven; the base class does not do this for you. This lets you report the actual achieved state, which may differ from the requested one. - Do not implement
turn_on()/turn_off()/toggle()- those are provided by the base class and ultimately call yourwrite_state().
Assumed state and restore mode
Section titled “Assumed state and restore mode”- If your switch cannot read back the real hardware state (so the reported state is only what ESPHome last wrote),
override
assumed_state()to returntrue. The front-end will then show separate ON and OFF buttons rather than a single toggle. - The base class handles the user’s
restore_mode:option (restoring the previous state from flash on boot). At the end ofsetup(), you can consultget_initial_state_with_restore_mode()to decide what state to apply.
Useful members
Section titled “Useful members”state: the current reported state.publish_state(bool): report a new state to the front-end (applies inversion, storesstate, fires callbacks).LOG_SWITCH(prefix, type, obj): a convenience macro fordump_config()that logs the switch in the standard format.
Exposing multiple switches from one component
Section titled “Exposing multiple switches from one component”Hardware often maps to more than one switch - a relay board might drive four independent relays, or a device hub might expose several unrelated feature toggles (a buzzer, a night light, a child lock). There are two established ways to model this. Both are valid and both are widely used in-tree - which one fits depends on how the hardware itself is shaped.
A hub plus a type on the platform
Section titled “A hub plus a type on the platform”If your component already has a top-level hub - configured once to describe the device or connection - you can let
the user add one switch: entry per relay, distinguished by a type key, using cv.typed_schema() with
key=CONF_TYPE:
relay_hub: id: the_hub
switch: - platform: relay_hub type: relay_1 name: "Relay 1" - platform: relay_hub type: relay_2 name: "Relay 2"from esphome.const import CONF_TYPE
from .. import CONF_RELAY_HUB_ID, RelayHub, relay_hub_ns
_HUB_ID_SCHEMA = cv.Schema({cv.GenerateID(CONF_RELAY_HUB_ID): cv.use_id(RelayHub)})
CONFIG_SCHEMA = cv.typed_schema( { "relay_1": switch.switch_schema(Relay1Switch).extend(_HUB_ID_SCHEMA), "relay_2": switch.switch_schema(Relay2Switch).extend(_HUB_ID_SCHEMA), }, key=CONF_TYPE,)
async def to_code(config): var = await switch.new_switch(config) await cg.register_parented(var, config[CONF_RELAY_HUB_ID])What this shape gives you:
- Each relay is a first-class platform entry, so it picks up the full set of per-entity options naturally.
- Adding a new relay later is purely additive - one more key in the
typed_schema- with no reshaping of the existing schema and no change to existing user configurations. - Different relays can use different C++ classes.
The switch platform of dfrobot_sen0395 is a real in-tree example - its type: selects which of four switches
(power, LED, UART presence, start-after-boot) a given entry controls.
Sub-configs on a single platform entry
Section titled “Sub-configs on a single platform entry”Many existing components instead nest optional switches under one platform entry. Unlike sensor.sensor_schema(),
switch.switch_schema() always requires a class, so this pattern still declares a dedicated class per switch - it is
just folded under one entry as an optional key instead of getting its own type:. ld2410 does this for its
engineering_mode and bluetooth switches:
switch: - platform: ld2410 ld2410_id: my_ld2410 engineering_mode: name: "Engineering Mode" bluetooth: name: "Bluetooth"On the C++ side, use the SUB_SWITCH(name) macro from switch.h, which generates a protected name##_switch_
member (defaulted to nullptr) and a public set_##name##_switch() setter:
class LD2410Component : public PollingComponent { public: SUB_SWITCH(engineering_mode) SUB_SWITCH(bluetooth)};Always null-check before use - the user may have configured only one of them.
In to_code, bind the hub as the parent once with switch.sub_switches(), then create each configured switch and
pass it to its setter:
hub = await cg.get_variable(config[CONF_LD2410_ID])switches = switch.sub_switches(config, parent=hub)await switches(CONF_ENGINEERING_MODE, hub.set_engineering_mode_switch)await switches(CONF_BLUETOOTH, hub.set_bluetooth_switch)Choosing between them
Section titled “Choosing between them”Neither pattern is the “modern” one and neither is deprecated. They express different relationships between a device and its switches, so let the hardware decide:
- Sub-configs fit when the switches are a small, fixed set that belong to a single chip - one physical device to one YAML block, which is easier to read.
- A hub plus
typefits when the switches are genuinely independent things that happen to share a connection. If a hub component already exists because the device or transport must be configured once and shared, then each switch being its own entry is the more natural fit - especially when the set is large or open-ended, or when different switches need different C++ classes or update strategies.
If both descriptions fit your device equally well, follow whichever pattern the surrounding component already uses.
See Exposing multiple sensors from one component for a fully worked example of both patterns.
For everything else, the component implements the usual set of methods as described here.
Copyright © 2026 ESPHome - A project from the Open Home Foundation