Cover
The cover component is one of ESPHome’s core entity types. A cover represents
anything that opens and closes and can be moved to a position - a garage door, a blind, a curtain, an awning, and so
on. It is a controllable entity: in addition to reporting its current position (and, optionally, tilt) it accepts
open/close/stop/toggle commands and can be asked to move to a specific position from the front-end (Home Assistant,
the web server, MQTT, etc.).
Like the other entity types, cover is a platform base. Individual components (for example a garage door relay, a
shutter motor controller, or a curtain motor) register a cover and implement the logic that actually drives the
hardware.
We have an example, minimal cover component which is a good starting point.
Python
Section titled “Python”A cover platform lives in a cover.py file (or a cover/__init__.py package) inside your component’s directory. This
file name tells ESPHome that the component provides a cover platform, allowing the user to configure it under the
cover: block:
cover: - platform: my_component name: "My Cover"The typical imports and class declaration look like this:
import esphome.codegen as cgimport esphome.config_validation as cvfrom esphome.components import cover
my_cover_ns = cg.esphome_ns.namespace("my_cover")MyCover = my_cover_ns.class_("MyCover", cover.Cover, cg.Component)Note that MyCover inherits from both cover.Cover and a component base (cg.Component here).
Configuration schema
Section titled “Configuration schema”Use the cover.cover_schema() helper. It returns a schema pre-populated with all of the options common to every
cover - name, id, icon, device_class, entity_category, the on_opened / on_closed / on_opening /
on_closing / on_idle automations, and so on:
CONFIG_SCHEMA = cover.cover_schema(MyCover).extend(cv.COMPONENT_SCHEMA)Passing your class (MyCover) 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 cover.new_cover() helper. It calls cg.new_Pvariable() for you and generates all the code to
apply the common cover options from the configuration:
async def to_code(config): var = await cover.new_cover(config) await cg.register_component(var, config)If your component owns a cover as a child (rather than being a cover), use await cover.register_cover(var, config)
instead, having declared the ID yourself.
The C++ class inherits from cover::Cover alongside its component base:
#include "esphome/core/component.h"#include "esphome/components/cover/cover.h"
namespace esphome::my_cover {
class MyCover : public cover::Cover, public Component { public: void dump_config() override; cover::CoverTraits get_traits() override;
protected: void control(const cover::CoverCall &call) override;};
} // namespace esphome::my_coverCommand flow
Section titled “Command flow”Unlike a switch, where the front-end calls a single public setter, a cover command
is built up as a cover::CoverCall object: the front-end (or your own code) calls make_call() to get one, sets whichever
fields it wants to change (set_position(), set_tilt(), set_command_open(), set_stop(), …), and then calls
perform() on it. This in turn dispatches to your protected virtual control() method with the finished call.
The two methods you must implement are control(const cover::CoverCall &call) and get_traits().
void MyCover::control(const cover::CoverCall &call) { if (call.get_stop()) { this->stop_hardware_(); this->current_operation = cover::COVER_OPERATION_IDLE; this->publish_state(); }
if (call.get_position().has_value()) { float pos = *call.get_position(); this->move_to_position_(pos); this->position = pos; this->publish_state(); }
if (call.get_tilt().has_value()) { this->tilt = *call.get_tilt(); this->publish_state(); }}
cover::CoverTraits MyCover::get_traits() { auto traits = cover::CoverTraits(); traits.set_supports_position(true); traits.set_supports_tilt(false); traits.set_is_assumed_state(false); return traits;}A few important details:
- Inspect
call.get_position(),call.get_tilt()andcall.get_stop()to find out what the caller actually asked for; only the fields the caller set will be present (get_position()/get_tilt()areoptional<float>, so checkhas_value()before dereferencing). get_position()andget_tilt()range from0.0(cover::COVER_CLOSED) to1.0(cover::COVER_OPEN) - use these constants rather than the raw literals where it improves readability.- After driving the hardware, update
this->position(andthis->tilt, if supported) and setthis->current_operationbefore callingpublish_state(), so the front-end sees an accurate, up-to-date state. get_traits()tells the front-end what the cover can do - whether it supports a continuousposition,tilt, being stopped mid-move, or only reports an assumed state (see below). Report only the capabilities your hardware actually has; the front-end adapts its UI accordingly (e.g. a cover that only supports open/close shows simple buttons instead of a position slider).
Assumed state
Section titled “Assumed state”If your cover cannot read back the real hardware position (so the reported position is only what ESPHome last
commanded), set traits.set_is_assumed_state(true) in get_traits(). The front-end will then show separate open/close
controls rather than trusting the reported position as ground truth.
Useful members
Section titled “Useful members”position: the current reported position,0.0(closed) to1.0(open). Usecover::COVER_OPEN/cover::COVER_CLOSEDfor the extremes.tilt: the current reported tilt, on the same0.0-1.0scale.current_operation: one ofcover::COVER_OPERATION_IDLE,cover::COVER_OPERATION_OPENING,cover::COVER_OPERATION_CLOSING.is_fully_open()/is_fully_closed(): convenience helpers comparingpositionagainst1.0/0.0.publish_state(bool save = true): report the currentposition/tilt/current_operationto the front-end; passsave = falseto skip persisting the state to flash.LOG_COVER(prefix, type, obj): a convenience macro fordump_config()that logs the cover in the standard format.
Exposing multiple covers from one component
Section titled “Exposing multiple covers from one component”Hardware sometimes drives more than one cover from a single controller - a multi-channel blind/shutter controller hub might expose a roller shade on one channel and a tilting venetian blind on another. 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, so read the two together and pick the one that describes your device more honestly.
A hub plus a type on the platform
Section titled “A hub plus a type on the platform”If your component has a top-level hub, let the user add one cover: entry per channel, distinguished by a type
key, using cv.typed_schema() with key=CONF_TYPE:
my_hub: id: the_hubcover: - platform: my_hub type: roller name: "Living Room Blind" - platform: my_hub type: venetian name: "Kitchen Blind"_HUB_ID_SCHEMA = cv.Schema({cv.GenerateID(CONF_MY_HUB_ID): cv.use_id(MyHub)})CONFIG_SCHEMA = cv.typed_schema( { "roller": cover.cover_schema(MyHubRollerCover).extend(_HUB_ID_SCHEMA), "venetian": cover.cover_schema(MyHubVenetianCover).extend(_HUB_ID_SCHEMA), }, key=CONF_TYPE,)
async def to_code(config): var = await cover.new_cover(config) await cg.register_component(var, config) await cg.register_parented(var, config[CONF_MY_HUB_ID])Each type gets its own schema and C++ class (only the venetian one needs set_supports_tilt(true) in
get_traits()), and adding a third type later is purely additive. See
Exposing multiple sensors from one component
for a fully worked example.
Sub-configs on a single platform entry
Section titled “Sub-configs on a single platform entry”Some components instead nest their channels as optional sub-keys under one platform entry, the way
dht nests temperature: and humidity:
under one sensor: entry. There is no SUB_COVER macro (only binary_sensor, button, number, select,
sensor, switch, text_sensor have one), and unlike sensor.sensor_schema(), cover.cover_schema() always
requires a class argument. Declare the pointer member and setter by hand, and null-check before use:
class MyHubComponent : public Component { public: void set_channel_0_cover(cover::Cover *cover) { this->channel_0_cover_ = cover; } protected: cover::Cover *channel_0_cover_{nullptr};};Choosing between them
Section titled “Choosing between them”Neither pattern is deprecated; they express different relationships between a controller and the covers it drives, so let the hardware decide:
- Sub-configs fit when the channels are a small, fixed set on a single controller board - one physical device maps to one YAML block, which is easier to read.
- A hub plus
typefits when a hub component already exists to describe the connection, and the set of channels is large or open-ended, or different channels need different traits (only the venetian type needsset_supports_tilt(true)) or C++ classes.
If both descriptions fit equally well, follow whichever pattern the surrounding component already uses. In a new component with no hub, sub-configs are usually the smaller change.
For everything else, the component implements the usual set of methods as described here.
Copyright © 2026 ESPHome - A project from the Open Home Foundation