mirror of
https://github.com/home-assistant/core.git
synced 2026-09-26 09:23:17 -04:00
Add cover speeds for SwitchBot curtains (#182416)
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude
parent
62c9711cdd
commit
71e382e3a8
@@ -261,6 +261,17 @@ ROLLER_SHADE_SPEED_TO_MODE = {
|
||||
ROLLER_SHADE_SPEED_QUIET: 1,
|
||||
}
|
||||
|
||||
# Curtain movement speeds, exposed as cover speeds.
|
||||
# Only the Curtain 3 (identified by its advertised model friendly name)
|
||||
# honours the speed byte; other curtain models always move at normal speed.
|
||||
CURTAIN_3_MODEL_FRIENDLY_NAME = "Curtain 3"
|
||||
CURTAIN_SPEED_NORMAL = "normal"
|
||||
CURTAIN_SPEED_SILENT = "silent"
|
||||
CURTAIN_SPEED_TO_VALUE = {
|
||||
CURTAIN_SPEED_NORMAL: 255,
|
||||
CURTAIN_SPEED_SILENT: 1,
|
||||
}
|
||||
|
||||
AIRPURIFIER_BASIC_MODELS = {
|
||||
SwitchbotModel.AIR_PURIFIER_JP,
|
||||
SwitchbotModel.AIR_PURIFIER_US,
|
||||
|
||||
@@ -20,6 +20,8 @@ from homeassistant.helpers.restore_state import RestoreEntity
|
||||
|
||||
from .const import (
|
||||
CONF_CURTAIN_SPEED,
|
||||
CURTAIN_3_MODEL_FRIENDLY_NAME,
|
||||
CURTAIN_SPEED_TO_VALUE,
|
||||
DEFAULT_CURTAIN_SPEED,
|
||||
ROLLER_SHADE_SPEED_PERFORMANCE,
|
||||
ROLLER_SHADE_SPEED_TO_MODE,
|
||||
@@ -67,10 +69,22 @@ class SwitchBotCurtainEntity(SwitchbotEntity, CoverEntity, RestoreEntity):
|
||||
"""Initialize the Switchbot."""
|
||||
super().__init__(coordinator)
|
||||
self._attr_is_closed = None
|
||||
# Only the Curtain 3 honours the speed byte; expose cover speeds
|
||||
# (e.g. silent mode) for that model only.
|
||||
if self._device.data.get("modelFriendlyName") == CURTAIN_3_MODEL_FRIENDLY_NAME:
|
||||
self._attr_supported_features |= CoverEntityFeature.SPEED
|
||||
self._attr_supported_speeds = list(CURTAIN_SPEED_TO_VALUE)
|
||||
|
||||
@callback
|
||||
def _get_curtain_speed(self) -> int:
|
||||
"""Return the configured curtain speed."""
|
||||
def _get_curtain_speed(self, kwargs: dict[str, Any]) -> int:
|
||||
"""Return the movement speed for the requested cover speed.
|
||||
|
||||
A speed passed to the action takes precedence; otherwise the speed
|
||||
configured in the options flow is used.
|
||||
"""
|
||||
if (speed := kwargs.get(ATTR_SPEED)) is not None:
|
||||
# The cover entity validated the speed against _attr_supported_speeds.
|
||||
return CURTAIN_SPEED_TO_VALUE[speed]
|
||||
return int(
|
||||
self.coordinator.config_entry.options.get(
|
||||
CONF_CURTAIN_SPEED, DEFAULT_CURTAIN_SPEED
|
||||
@@ -101,7 +115,7 @@ class SwitchBotCurtainEntity(SwitchbotEntity, CoverEntity, RestoreEntity):
|
||||
"""Open the curtain."""
|
||||
|
||||
_LOGGER.debug("Switchbot to open curtain %s", self._address)
|
||||
speed = self._get_curtain_speed()
|
||||
speed = self._get_curtain_speed(kwargs)
|
||||
self._last_run_success = bool(await self._device.open(speed))
|
||||
self._attr_is_opening = self._device.is_opening()
|
||||
self._attr_is_closing = self._device.is_closing()
|
||||
@@ -113,7 +127,7 @@ class SwitchBotCurtainEntity(SwitchbotEntity, CoverEntity, RestoreEntity):
|
||||
"""Close the curtain."""
|
||||
|
||||
_LOGGER.debug("Switchbot to close the curtain %s", self._address)
|
||||
speed = self._get_curtain_speed()
|
||||
speed = self._get_curtain_speed(kwargs)
|
||||
self._last_run_success = bool(await self._device.close(speed))
|
||||
self._attr_is_opening = self._device.is_opening()
|
||||
self._attr_is_closing = self._device.is_closing()
|
||||
@@ -134,10 +148,11 @@ class SwitchBotCurtainEntity(SwitchbotEntity, CoverEntity, RestoreEntity):
|
||||
@override
|
||||
async def async_set_cover_position(self, **kwargs: Any) -> None:
|
||||
"""Move the cover shutter to a specific position."""
|
||||
position = kwargs.get(ATTR_POSITION)
|
||||
position = kwargs[ATTR_POSITION]
|
||||
|
||||
_LOGGER.debug("Switchbot to move at %d %s", position, self._address)
|
||||
self._last_run_success = bool(await self._device.set_position(position))
|
||||
speed = self._get_curtain_speed(kwargs)
|
||||
self._last_run_success = bool(await self._device.set_position(position, speed))
|
||||
self._attr_is_opening = self._device.is_opening()
|
||||
self._attr_is_closing = self._device.is_closing()
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -157,6 +157,12 @@
|
||||
"false": "[%key:component::binary_sensor::entity_component::problem::state::on%]",
|
||||
"true": "[%key:component::binary_sensor::entity_component::problem::state::off%]"
|
||||
}
|
||||
},
|
||||
"speed": {
|
||||
"state": {
|
||||
"normal": "Normal",
|
||||
"silent": "Silent"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -15,17 +15,21 @@ from homeassistant.components.cover import (
|
||||
ATTR_TILT_POSITION,
|
||||
DOMAIN as COVER_DOMAIN,
|
||||
CoverEntityCapabilityAttribute,
|
||||
CoverEntityFeature,
|
||||
CoverState,
|
||||
)
|
||||
from homeassistant.components.switchbot.const import (
|
||||
CONF_CURTAIN_SPEED,
|
||||
CONF_RETRY_COUNT,
|
||||
CURTAIN_SPEED_NORMAL,
|
||||
CURTAIN_SPEED_SILENT,
|
||||
DEFAULT_RETRY_COUNT,
|
||||
ROLLER_SHADE_SPEED_PERFORMANCE,
|
||||
ROLLER_SHADE_SPEED_QUIET,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_SUPPORTED_FEATURES,
|
||||
SERVICE_CLOSE_COVER,
|
||||
SERVICE_CLOSE_COVER_TILT,
|
||||
SERVICE_OPEN_COVER,
|
||||
@@ -176,7 +180,7 @@ async def test_curtain3_controlling(
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
mock_set_position.assert_awaited_once()
|
||||
mock_set_position.assert_awaited_once_with(50, 255) # Default speed
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == CoverState.OPEN
|
||||
assert state.attributes[ATTR_CURRENT_POSITION] == 60
|
||||
@@ -231,6 +235,116 @@ async def test_curtain3_custom_speed_controlling(
|
||||
mock_close.assert_awaited_once_with(50)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("speed", "expected_value"),
|
||||
[
|
||||
pytest.param(CURTAIN_SPEED_NORMAL, 255, id="normal"),
|
||||
pytest.param(CURTAIN_SPEED_SILENT, 1, id="silent"),
|
||||
],
|
||||
)
|
||||
async def test_curtain3_speed(
|
||||
hass: HomeAssistant,
|
||||
mock_entry_factory: Callable[[str], MockConfigEntry],
|
||||
speed: str,
|
||||
expected_value: int,
|
||||
) -> None:
|
||||
"""Test the curtain forwards the requested cover speed to the device."""
|
||||
inject_bluetooth_service_info(hass, WOCURTAIN3_SERVICE_INFO)
|
||||
|
||||
entry = mock_entry_factory(sensor_type="curtain")
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.switchbot.cover.switchbot.SwitchbotCurtain.open",
|
||||
new=AsyncMock(return_value=True),
|
||||
) as mock_open,
|
||||
patch(
|
||||
"homeassistant.components.switchbot.cover.switchbot.SwitchbotCurtain.close",
|
||||
new=AsyncMock(return_value=True),
|
||||
) as mock_close,
|
||||
patch(
|
||||
"homeassistant.components.switchbot.cover.switchbot.SwitchbotCurtain.set_position",
|
||||
new=AsyncMock(return_value=True),
|
||||
) as mock_set_position,
|
||||
):
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = "cover.test_name"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.attributes[CoverEntityCapabilityAttribute.SUPPORTED_SPEEDS] == [
|
||||
CURTAIN_SPEED_NORMAL,
|
||||
CURTAIN_SPEED_SILENT,
|
||||
]
|
||||
|
||||
await hass.services.async_call(
|
||||
COVER_DOMAIN,
|
||||
SERVICE_OPEN_COVER,
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_SPEED: speed},
|
||||
blocking=True,
|
||||
)
|
||||
mock_open.assert_awaited_once_with(expected_value)
|
||||
|
||||
await hass.services.async_call(
|
||||
COVER_DOMAIN,
|
||||
SERVICE_CLOSE_COVER,
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_SPEED: speed},
|
||||
blocking=True,
|
||||
)
|
||||
mock_close.assert_awaited_once_with(expected_value)
|
||||
|
||||
await hass.services.async_call(
|
||||
COVER_DOMAIN,
|
||||
SERVICE_SET_COVER_POSITION,
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_POSITION: 50, ATTR_SPEED: speed},
|
||||
blocking=True,
|
||||
)
|
||||
mock_set_position.assert_awaited_once_with(50, expected_value)
|
||||
|
||||
|
||||
async def test_curtain_speed_not_supported(
|
||||
hass: HomeAssistant, mock_entry_factory: Callable[[str], MockConfigEntry]
|
||||
) -> None:
|
||||
"""Test a non-Curtain-3 curtain does not expose the speed feature."""
|
||||
# A Curtain (not Curtain 3) advertises the "c" device type in its service data.
|
||||
inject_bluetooth_service_info(
|
||||
hass,
|
||||
make_advertisement(
|
||||
"AA:BB:CC:DD:EE:FF",
|
||||
b"\xcf;Zwu\x0c\x19\x0b\x00\x11D\x006",
|
||||
b"c\xc06\x00\x11D",
|
||||
),
|
||||
)
|
||||
|
||||
entry = mock_entry_factory(sensor_type="curtain")
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.switchbot.cover.switchbot.SwitchbotCurtain.open",
|
||||
new=AsyncMock(return_value=True),
|
||||
) as mock_open:
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = "cover.test_name"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert not state.attributes[ATTR_SUPPORTED_FEATURES] & CoverEntityFeature.SPEED
|
||||
assert CoverEntityCapabilityAttribute.SUPPORTED_SPEEDS not in state.attributes
|
||||
|
||||
# A speed passed to a model that does not support it is ignored and the
|
||||
# curtain moves at the default speed.
|
||||
await hass.services.async_call(
|
||||
COVER_DOMAIN,
|
||||
SERVICE_OPEN_COVER,
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_SPEED: CURTAIN_SPEED_SILENT},
|
||||
blocking=True,
|
||||
)
|
||||
mock_open.assert_awaited_once_with(255)
|
||||
|
||||
|
||||
async def test_blindtilt_setup(
|
||||
hass: HomeAssistant, mock_entry_factory: Callable[[str], MockConfigEntry]
|
||||
) -> None:
|
||||
|
||||
Reference in New Issue
Block a user