Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
349ae63
add optional speed parameter to cover movement ops
wollew Jul 10, 2026
0c07a0c
remove callback annotation
wollew Jul 10, 2026
40fc350
Merge branch 'dev' into cover_speed_simplified_proposal
wollew Jul 10, 2026
a517db0
use state selector instead of text
wollew Jul 11, 2026
6dc6227
Merge branch 'dev' into cover_speed_simplified_proposal
wollew Jul 11, 2026
3e0656c
remove superfluous comment
wollew Jul 11, 2026
9aa818b
remove superfluous comments
wollew Jul 11, 2026
979f73e
language improvement
wollew Jul 11, 2026
976a8ad
apply review comments
wollew Jul 11, 2026
3beb006
review comment, mapping needs to be done in FE
wollew Jul 12, 2026
3125b78
factor out field name
wollew Jul 12, 2026
dc80f60
fix typo
wollew Jul 12, 2026
f143781
sort names
wollew Jul 13, 2026
2020094
do not use specific exception if speed is unsupported
wollew Aug 10, 2026
a92d470
strip speed from kwargs if speed is not supported by entity.
wollew Aug 10, 2026
6aff465
address review comment
wollew Aug 10, 2026
57386d9
inline helper
wollew Aug 10, 2026
f6437a7
Merge branch 'dev' into cover_speed_simplified_proposal
wollew Aug 10, 2026
78b683b
review comment
wollew Aug 17, 2026
f610c9a
review comment: simplify code
wollew Aug 17, 2026
362460b
fix missing import
wollew Aug 17, 2026
21fa59c
Potential fix for pull request finding
wollew Aug 17, 2026
bc4faf2
add speed support to demo cover
wollew Aug 17, 2026
6cd651d
Merge branch 'dev' into cover_speed_simplified_proposal
wollew Aug 17, 2026
a169847
review comment: consistent naming of speeds
wollew Aug 17, 2026
61b601c
round set_position to multiples of 5, clamp to (0,100)
wollew Aug 17, 2026
b29b1e4
Potential fix for pull request finding
wollew Aug 17, 2026
0c8cf3f
add coverentityfeature for speed support and filterfor it
wollew Aug 18, 2026
4388df6
Potential fix for pull request finding
wollew Aug 18, 2026
20b0565
fix test
wollew Aug 19, 2026
af13c60
Merge branch 'dev' into cover_speed_simplified_proposal
wollew Aug 19, 2026
f70a4dc
Merge branch 'dev' into cover_speed_simplified_proposal
wollew Aug 19, 2026
1cd0e1f
translate speed in demo cover component
wollew Aug 19, 2026
59919a5
use constant instead of literal
wollew Aug 19, 2026
22e02c8
Merge branch 'dev' into cover_speed_simplified_proposal
wollew Aug 19, 2026
e155949
revert most changes in demo cover, speed parameter exists but is pure…
wollew Aug 20, 2026
2e86f05
Merge branch 'dev' into cover_speed_simplified_proposal
wollew Aug 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 94 additions & 4 deletions homeassistant/components/cover/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
SERVICE_TOGGLE_COVER_TILT,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.entity_component import EntityComponent
Expand All @@ -35,11 +36,13 @@
ATTR_CURRENT_TILT_POSITION,
ATTR_IS_CLOSED,
ATTR_POSITION,
ATTR_SPEED,
ATTR_TILT_POSITION,
DOMAIN,
INTENT_CLOSE_COVER,
INTENT_OPEN_COVER,
CoverDeviceClass,
CoverEntityCapabilityAttribute,
CoverEntityFeature,
CoverEntityStateAttribute,
CoverState,
Expand All @@ -65,6 +68,7 @@
"ATTR_CURRENT_TILT_POSITION",
"ATTR_IS_CLOSED",
"ATTR_POSITION",
"ATTR_SPEED",
"ATTR_TILT_POSITION",
"DEVICE_CLASSES",
"DEVICE_CLASSES_SCHEMA",
Expand All @@ -75,6 +79,7 @@
"PLATFORM_SCHEMA_BASE",
"CoverDeviceClass",
"CoverEntity",
"CoverEntityCapabilityAttribute",
"CoverEntityDescription",
"CoverEntityFeature",
"CoverEntityStateAttribute",
Expand All @@ -100,21 +105,28 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
await component.async_setup(config)

component.async_register_entity_service(
SERVICE_OPEN_COVER, None, "async_open_cover", [CoverEntityFeature.OPEN]
SERVICE_OPEN_COVER,
{vol.Optional(ATTR_SPEED): cv.string},
"async_handle_open_cover",
[CoverEntityFeature.OPEN],
)

component.async_register_entity_service(
SERVICE_CLOSE_COVER, None, "async_close_cover", [CoverEntityFeature.CLOSE]
SERVICE_CLOSE_COVER,
{vol.Optional(ATTR_SPEED): cv.string},
"async_handle_close_cover",
[CoverEntityFeature.CLOSE],
)

component.async_register_entity_service(
SERVICE_SET_COVER_POSITION,
{
vol.Required(ATTR_POSITION): vol.All(
vol.Coerce(int), vol.Range(min=0, max=100)
)
),
vol.Optional(ATTR_SPEED): cv.string,
},
"async_set_cover_position",
"async_handle_set_cover_position",
[CoverEntityFeature.SET_POSITION],
)

Expand Down Expand Up @@ -194,6 +206,7 @@ class CoverEntityDescription(EntityDescription, frozen_or_thawed=True):
"is_opening",
"is_closing",
"is_closed",
"supported_speeds",
}


Expand All @@ -209,9 +222,16 @@ class CoverEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
_attr_is_opening: bool | None = None
_attr_state: None = None
_attr_supported_features: CoverEntityFeature | None
_attr_supported_speeds: list[str] | None = None

_cover_is_last_toggle_direction_open = True

_entity_component_unrecorded_attributes = frozenset(
{
CoverEntityCapabilityAttribute.SUPPORTED_SPEEDS,
}
)

@cached_property
def current_cover_position(self) -> int | None:
"""Return current position of cover.
Expand Down Expand Up @@ -283,6 +303,9 @@ def supported_features(self) -> CoverEntityFeature:
CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE | CoverEntityFeature.STOP
)

if self.supported_speeds:
supported_features |= CoverEntityFeature.SPEED

if self.current_cover_position is not None:
supported_features |= CoverEntityFeature.SET_POSITION

Expand Down Expand Up @@ -311,6 +334,73 @@ def is_closed(self) -> bool | None:
"""Return if the cover is closed or not."""
return self._attr_is_closed

@cached_property
def supported_speeds(self) -> list[str] | None:
"""Return the list of speeds supported for open/close/set_position operations.

None or an empty list means speed selection is not supported.
"""
return self._attr_supported_speeds

@property
@override
def capability_attributes(self) -> dict[str, Any] | None:
"""Return capability attributes."""

if speeds := self.supported_speeds:
return {CoverEntityCapabilityAttribute.SUPPORTED_SPEEDS: speeds}
return None

@final
def _valid_speed_or_raise(self, speed: str, supported: list[str]) -> None:
"""Raise ServiceValidationError if speed is not in the supported list."""
if speed not in supported:
supported_str = ", ".join(supported)
raise ServiceValidationError(
translation_key="not_valid_speed",
translation_domain=DOMAIN,
translation_placeholders={
"speed": speed,
"supported_speeds": supported_str,
},
)

@final
async def async_handle_open_cover(self, **kwargs: Any) -> None:
"""Validate speed and open the cover."""
call_kwargs = kwargs
if (speed := kwargs.get(ATTR_SPEED)) is not None:
if speeds := self.supported_speeds:
self._valid_speed_or_raise(speed, speeds)
else:
call_kwargs = dict(kwargs)
call_kwargs.pop(ATTR_SPEED)
await self.async_open_cover(**call_kwargs)

@final
async def async_handle_close_cover(self, **kwargs: Any) -> None:
"""Validate speed and close the cover."""
call_kwargs = kwargs
if (speed := kwargs.get(ATTR_SPEED)) is not None:
if speeds := self.supported_speeds:
self._valid_speed_or_raise(speed, speeds)
else:
call_kwargs = dict(kwargs)
call_kwargs.pop(ATTR_SPEED)
await self.async_close_cover(**call_kwargs)

@final
async def async_handle_set_cover_position(self, **kwargs: Any) -> None:
"""Validate speed and move the cover to a specific position."""
call_kwargs = kwargs
if (speed := kwargs.get(ATTR_SPEED)) is not None:
if speeds := self.supported_speeds:
self._valid_speed_or_raise(speed, speeds)
else:
call_kwargs = dict(kwargs)
call_kwargs.pop(ATTR_SPEED)
await self.async_set_cover_position(**call_kwargs)

def open_cover(self, **kwargs: Any) -> None:
"""Open the cover."""
raise NotImplementedError
Expand Down
8 changes: 8 additions & 0 deletions homeassistant/components/cover/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,16 @@
ATTR_CURRENT_TILT_POSITION = "current_tilt_position"
ATTR_IS_CLOSED = "is_closed"
ATTR_POSITION = "position"
ATTR_SPEED = "speed"
ATTR_TILT_POSITION = "tilt_position"


class CoverEntityCapabilityAttribute(StrEnum):
"""Capability attributes for cover entities."""

SUPPORTED_SPEEDS = "supported_speeds"


class CoverEntityStateAttribute(StrEnum):
"""State attributes for cover entities."""

Expand All @@ -34,6 +41,7 @@ class CoverEntityFeature(IntFlag):
CLOSE_TILT = 32
STOP_TILT = 64
SET_TILT_POSITION = 128
SPEED = 256


class CoverState(StrEnum):
Expand Down
26 changes: 26 additions & 0 deletions homeassistant/components/cover/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,31 @@ open_cover:
domain: cover
supported_features:
- cover.CoverEntityFeature.OPEN
fields:
speed:
example: "fast"
filter:
supported_features:
- cover.CoverEntityFeature.SPEED
selector:
state:
attribute: speed
Comment thread
wollew marked this conversation as resolved.
Comment thread
wollew marked this conversation as resolved.
Comment on lines +10 to +17

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be filtered for entities that don't support speeds?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that possible? The docs say I need to specify at least one value but these are all unknown in this case. And I also could not find an example. Or did I misunderstand this?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it would be possible if there was a supported_feature flag for speeds?

I just worry it looks bad when entities without speeds show a speed dropdown that is just blank/empty. It might confuse people into thinking somehow they could change the speed and they wouldn't know what to do with it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it would be possible if there was a supported_feature flag for speeds?

I think so, but I understood home-assistant/architecture#789 (reply in thread) as "we don't need that". It seems we do, do you want me to change it?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will leave that decision up to the core reviewers. It may be that when it was first thought that it wasn't needed, this wasn't considered.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

supported_features flag maps to services and this is not (it's a parameter to an existing service).

But I do see two "problems" with this implementation;

  1. As above, the list is dynamic so filtering does not work
  2. The speed choice is to be selected every time you use the service, there's no saving of your choice, so how to present that in a nice/user-friendly way in frontend I'm not sure. Perhaps not at all.

@MartinHjelmare what do you think?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you elaborate what you mean by "supported_features maps to services"? It seems like it's used in lots more ways than just that.

Is there a rule that supported feature is only allowed if it gates an entire service? Seems weirdly restrictive, given it is also used for other things.

  • Light has TRANSITION, but that does not gate a service.
  • Todo has SET_DESCRIPTION_ON_ITEM, but that does not gate a service.
  • Siren has TONES, VOLUME_SET, DURATION, but these do not gate a service.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, misunderstanding from my side. I think we need to have an entity feature flag for speed 👍🏼

@wollew wollew Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added the feature flag and filtering

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, speed should be a supported feature. It is used to express what service actions and what action parameters are supported.

Comment thread
wollew marked this conversation as resolved.
Comment thread
wollew marked this conversation as resolved.
Comment thread
wollew marked this conversation as resolved.
Comment thread
wollew marked this conversation as resolved.
Comment thread
karwosts marked this conversation as resolved.

close_cover:
target:
entity:
domain: cover
supported_features:
- cover.CoverEntityFeature.CLOSE
fields:
speed:
example: "fast"
filter:
supported_features:
- cover.CoverEntityFeature.SPEED
selector:
state:
attribute: speed
Comment thread
karwosts marked this conversation as resolved.

toggle:
target:
Expand All @@ -36,6 +54,14 @@ set_cover_position:
min: 0
max: 100
unit_of_measurement: "%"
speed:
filter:
supported_features:
- cover.CoverEntityFeature.SPEED
example: "fast"
selector:
state:
attribute: speed
Comment thread
karwosts marked this conversation as resolved.

stop_cover:
target:
Expand Down
22 changes: 22 additions & 0 deletions homeassistant/components/cover/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"common": {
"condition_behavior_name": "Condition passes if",
"condition_for_name": "For at least",
"field_speed_name": "Speed",
"trigger_behavior_name": "Trigger when",
"trigger_for_name": "For at least"
},
Expand Down Expand Up @@ -210,9 +211,20 @@
"name": "Window"
}
},
"exceptions": {
"not_valid_speed": {
"message": "Speed {speed} is not valid. Valid speeds are: {supported_speeds}."
}
},
"services": {
"close_cover": {
"description": "Closes a cover.",
"fields": {
"speed": {
"description": "Speed at which to close the cover.",
"name": "[%key:component::cover::common::field_speed_name%]"
}
},
"name": "Close cover"
},
"close_cover_tilt": {
Expand All @@ -221,6 +233,12 @@
},
"open_cover": {
"description": "Opens a cover.",
"fields": {
"speed": {
"description": "Speed at which to open the cover.",
"name": "[%key:component::cover::common::field_speed_name%]"
}
},
"name": "Open cover"
},
"open_cover_tilt": {
Expand All @@ -233,6 +251,10 @@
"position": {
"description": "Target position.",
"name": "Position"
},
"speed": {
"description": "Speed at which to move the cover.",
"name": "[%key:component::cover::common::field_speed_name%]"
}
},
"name": "Set cover position"
Expand Down
14 changes: 13 additions & 1 deletion homeassistant/components/demo/cover.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,15 @@ async def async_setup_entry(
[
DemoCover(hass, "cover_1", "Kitchen Window"),
DemoCover(hass, "cover_2", "Hall Window", 10),
DemoCover(hass, "cover_3", "Living Room Window", 70, 50),
DemoCover(
hass,
"cover_3",
"Living Room Window",
70,
50,
supported_speeds=["slow", "default"],
translation_key="living_room_window",
),
DemoCover(
hass,
"cover_4",
Expand Down Expand Up @@ -69,13 +77,17 @@ def __init__(
tilt_position: int | None = None,
device_class: CoverDeviceClass | None = None,
supported_features: CoverEntityFeature | None = None,
supported_speeds: list[str] | None = None,
translation_key: str | None = None,
) -> None:
"""Initialize the cover."""
self.hass = hass
self._unique_id = unique_id
self._position = position
self._attr_device_class = device_class
self._attr_supported_features = supported_features
self._attr_supported_speeds = supported_speeds
self._attr_translation_key = translation_key
self._set_position: int | None = None
self._set_tilt_position: int | None = None
self._tilt_position = tilt_position
Expand Down
12 changes: 12 additions & 0 deletions homeassistant/components/demo/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,18 @@
}
}
},
"cover": {
"living_room_window": {
"state_attributes": {
"speed": {
"state": {
"default": "Default",
"slow": "Slow"
}
}
}
}
},
"event": {
"push": {
"state_attributes": {
Expand Down
Loading
Loading