diff --git a/homeassistant/components/overkiz/cover.py b/homeassistant/components/overkiz/cover.py new file mode 100644 index 0000000000000..c6a7bebd805a8 --- /dev/null +++ b/homeassistant/components/overkiz/cover.py @@ -0,0 +1,641 @@ +"""Support for Overkiz covers - shutters etc.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from pyoverkiz.enums import ( + OverkizCommand, + OverkizCommandParam, + OverkizState, + UIClass, + UIWidget, +) +from pyoverkiz.types import StateType as OverkizStateType + +from homeassistant.components.cover import ( + ATTR_POSITION, + ATTR_TILT_POSITION, + CoverDeviceClass, + CoverEntity, + CoverEntityDescription, + CoverEntityFeature, +) +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from . import OverkizDataConfigEntry +from .const import LOGGER +from .coordinator import OverkizDataUpdateCoordinator +from .entity import OverkizDescriptiveEntity + +# Special position values reported by some Overkiz devices +_POSITION_MY = 108 # "My position" preset +_POSITION_UNKNOWN = 124 # "Unknown position" preset + + +@dataclass(frozen=True, kw_only=True) +class OverkizCoverDescription(CoverEntityDescription): + """Class to describe an Overkiz cover.""" + + open_command: OverkizCommand | None = None + close_command: OverkizCommand | None = None + stop_command: OverkizCommand | None = None + current_position_state: OverkizState | None = None + invert_position: bool = True + set_position_command: OverkizCommand | None = None + is_closed_state: OverkizState | None = None + current_tilt_position_state: OverkizState | None = None + invert_tilt_position: bool = True + set_tilt_position_command: OverkizCommand | None = None + open_tilt_command: OverkizCommand | None = None + open_tilt_command_args: tuple[OverkizStateType, ...] = () + close_tilt_command: OverkizCommand | None = None + close_tilt_command_args: tuple[OverkizStateType, ...] = () + stop_tilt_command: OverkizCommand | None = None + + +COVER_DESCRIPTIONS: list[OverkizCoverDescription] = [ + ## + ## Overrides via UIWidget + ## + # Needs override to support position (and remove support for tilt position which is not supported by this device) + # uiClass is Pergola + OverkizCoverDescription( + key=UIWidget.PERGOLA_HORIZONTAL_AWNING, + device_class=CoverDeviceClass.AWNING, + current_position_state=OverkizState.CORE_DEPLOYMENT, + set_position_command=OverkizCommand.SET_DEPLOYMENT, + open_command=OverkizCommand.DEPLOY, + close_command=OverkizCommand.UNDEPLOY, + invert_position=False, + is_closed_state=OverkizState.CORE_OPEN_CLOSED, + ), + OverkizCoverDescription( + key=UIWidget.PERGOLA_HORIZONTAL_AWNING_UNO, + device_class=CoverDeviceClass.AWNING, + current_position_state=OverkizState.CORE_DEPLOYMENT, + set_position_command=OverkizCommand.SET_DEPLOYMENT, + open_command=OverkizCommand.DEPLOY, + close_command=OverkizCommand.UNDEPLOY, + invert_position=False, + is_closed_state=OverkizState.CORE_OPEN_CLOSED, + ), + # Needs override to support lower/upper position control + # uiClass is RollerShutter + OverkizCoverDescription( + key=UIWidget.POSITIONABLE_DUAL_ROLLER_SHUTTER, + device_class=CoverDeviceClass.SHUTTER, + current_position_state=OverkizState.CORE_UPPER_CLOSURE, + set_position_command=OverkizCommand.SET_UPPER_CLOSURE, + open_command=OverkizCommand.UPPER_OPEN, + close_command=OverkizCommand.UPPER_CLOSE, + stop_command=OverkizCommand.STOP, + is_closed_state=OverkizState.CORE_UPPER_OPEN_CLOSED, + # Lower position used as tilt (no separate tilt state) + current_tilt_position_state=OverkizState.CORE_LOWER_CLOSURE, + set_tilt_position_command=OverkizCommand.SET_LOWER_CLOSURE, + open_tilt_command=OverkizCommand.LOWER_OPEN, + close_tilt_command=OverkizCommand.LOWER_CLOSE, + stop_tilt_command=OverkizCommand.STOP, + ), + # Needs override to remove open/close commands + # uiClass is VenetianBlind + OverkizCoverDescription( + key=UIWidget.TILT_ONLY_VENETIAN_BLIND, + device_class=CoverDeviceClass.BLIND, + is_closed_state=OverkizState.CORE_OPEN_CLOSED, + open_tilt_command=OverkizCommand.TILT_POSITIVE, + close_tilt_command=OverkizCommand.TILT_NEGATIVE, + stop_tilt_command=OverkizCommand.STOP, + ), + # Needs override to support very specific tilt commands (rts:ExteriorVenetianBlindRTSComponent) + # uiClass is ExteriorVenetianBlind + OverkizCoverDescription( + key=UIWidget.UP_DOWN_EXTERIOR_VENETIAN_BLIND, + device_class=CoverDeviceClass.BLIND, + open_command=OverkizCommand.OPEN, + close_command=OverkizCommand.CLOSE, + stop_command=OverkizCommand.STOP, + open_tilt_command=OverkizCommand.TILT_POSITIVE, + open_tilt_command_args=(15, 1), # position (1-127), speed (1-15) + close_tilt_command=OverkizCommand.TILT_NEGATIVE, + close_tilt_command_args=(15, 1), # position (1-127), speed (1-15) + stop_tilt_command=OverkizCommand.STOP, + ), + # Needs override to support this Generic device (rts:GenericRTSComponent) + # uiClass is Generic (not mapped to cover as this is a Generic device class) + OverkizCoverDescription( + key=UIWidget.RTS_GENERIC, + open_command=OverkizCommand.OPEN, + close_command=OverkizCommand.CLOSE, + stop_command=OverkizCommand.STOP, + ), + ## + ## Default cover behavior (via UIClass) + ## + OverkizCoverDescription( + key=UIClass.ADJUSTABLE_SLATS_ROLLER_SHUTTER, + device_class=CoverDeviceClass.BLIND, + current_position_state=OverkizState.CORE_CLOSURE, + set_position_command=OverkizCommand.SET_CLOSURE, + open_command=OverkizCommand.OPEN, + close_command=OverkizCommand.CLOSE, + stop_command=OverkizCommand.STOP, + current_tilt_position_state=OverkizState.CORE_SLATE_ORIENTATION, + set_tilt_position_command=OverkizCommand.SET_ORIENTATION, + stop_tilt_command=OverkizCommand.STOP, + ), + OverkizCoverDescription( + key=UIClass.AWNING, + device_class=CoverDeviceClass.AWNING, + current_position_state=OverkizState.CORE_DEPLOYMENT, + set_position_command=OverkizCommand.SET_DEPLOYMENT, + open_command=OverkizCommand.DEPLOY, + close_command=OverkizCommand.UNDEPLOY, + invert_position=False, + is_closed_state=OverkizState.CORE_OPEN_CLOSED, + stop_command=OverkizCommand.STOP, + ), + OverkizCoverDescription( + key=UIClass.CURTAIN, + device_class=CoverDeviceClass.CURTAIN, + current_position_state=OverkizState.CORE_CLOSURE, + set_position_command=OverkizCommand.SET_CLOSURE, + open_command=OverkizCommand.OPEN, + close_command=OverkizCommand.CLOSE, + is_closed_state=OverkizState.CORE_OPEN_CLOSED, + stop_command=OverkizCommand.STOP, + ), + OverkizCoverDescription( + key=UIClass.EXTERIOR_SCREEN, + device_class=CoverDeviceClass.BLIND, + current_position_state=OverkizState.CORE_CLOSURE, + set_position_command=OverkizCommand.SET_CLOSURE, + open_command=OverkizCommand.OPEN, + close_command=OverkizCommand.CLOSE, + is_closed_state=OverkizState.CORE_OPEN_CLOSED, + stop_command=OverkizCommand.STOP, + ), + OverkizCoverDescription( + key=UIClass.EXTERIOR_VENETIAN_BLIND, + device_class=CoverDeviceClass.BLIND, + current_position_state=OverkizState.CORE_CLOSURE, + set_position_command=OverkizCommand.SET_CLOSURE, + open_command=OverkizCommand.OPEN, + close_command=OverkizCommand.CLOSE, + is_closed_state=OverkizState.CORE_OPEN_CLOSED, + stop_command=OverkizCommand.STOP, + current_tilt_position_state=OverkizState.CORE_SLATE_ORIENTATION, + set_tilt_position_command=OverkizCommand.SET_ORIENTATION, + open_tilt_command=OverkizCommand.TILT_DOWN, + close_tilt_command=OverkizCommand.TILT_UP, + stop_tilt_command=OverkizCommand.STOP, + ), + OverkizCoverDescription( + key=UIClass.GARAGE_DOOR, + device_class=CoverDeviceClass.GARAGE, + current_position_state=OverkizState.CORE_CLOSURE, + set_position_command=OverkizCommand.SET_CLOSURE, + open_command=OverkizCommand.OPEN, + close_command=OverkizCommand.CLOSE, + is_closed_state=OverkizState.CORE_OPEN_CLOSED_UNKNOWN, + stop_command=OverkizCommand.STOP, + ), + OverkizCoverDescription( + key=UIClass.GATE, + device_class=CoverDeviceClass.GATE, + open_command=OverkizCommand.OPEN, + close_command=OverkizCommand.CLOSE, + is_closed_state=OverkizState.CORE_OPEN_CLOSED_PEDESTRIAN, + stop_command=OverkizCommand.STOP, + ), + OverkizCoverDescription( + key=UIClass.PERGOLA, + device_class=CoverDeviceClass.AWNING, + is_closed_state=OverkizState.CORE_SLATS_OPEN_CLOSED, + current_tilt_position_state=OverkizState.CORE_SLATE_ORIENTATION, + set_tilt_position_command=OverkizCommand.SET_ORIENTATION, + open_tilt_command=OverkizCommand.OPEN_SLATS, + close_tilt_command=OverkizCommand.CLOSE_SLATS, + stop_tilt_command=OverkizCommand.STOP, + ), + OverkizCoverDescription( + key=UIClass.ROLLER_SHUTTER, + device_class=CoverDeviceClass.SHUTTER, + current_position_state=OverkizState.CORE_CLOSURE, + set_position_command=OverkizCommand.SET_CLOSURE, + open_command=OverkizCommand.OPEN, + close_command=OverkizCommand.CLOSE, + is_closed_state=OverkizState.CORE_OPEN_CLOSED, + stop_command=OverkizCommand.STOP, + ), + OverkizCoverDescription( + key=UIClass.SCREEN, + device_class=CoverDeviceClass.BLIND, + current_position_state=OverkizState.CORE_CLOSURE, + set_position_command=OverkizCommand.SET_CLOSURE, + open_command=OverkizCommand.OPEN, + close_command=OverkizCommand.CLOSE, + is_closed_state=OverkizState.CORE_OPEN_CLOSED, + stop_command=OverkizCommand.STOP, + ), + OverkizCoverDescription( + key=UIClass.SHUTTER, + device_class=CoverDeviceClass.SHUTTER, + current_position_state=OverkizState.CORE_CLOSURE, + set_position_command=OverkizCommand.SET_CLOSURE, + open_command=OverkizCommand.OPEN, + close_command=OverkizCommand.CLOSE, + is_closed_state=OverkizState.CORE_OPEN_CLOSED, + stop_command=OverkizCommand.STOP, + ), + OverkizCoverDescription( + key=UIClass.SWINGING_SHUTTER, + device_class=CoverDeviceClass.SHUTTER, + current_position_state=OverkizState.CORE_CLOSURE, + set_position_command=OverkizCommand.SET_CLOSURE, + open_command=OverkizCommand.OPEN, + close_command=OverkizCommand.CLOSE, + is_closed_state=OverkizState.CORE_OPEN_CLOSED, + stop_command=OverkizCommand.STOP, + ), + OverkizCoverDescription( + key=UIClass.VENETIAN_BLIND, + device_class=CoverDeviceClass.BLIND, + open_command=OverkizCommand.OPEN, + close_command=OverkizCommand.CLOSE, + is_closed_state=OverkizState.CORE_OPEN_CLOSED, + open_tilt_command=OverkizCommand.TILT_UP, + close_tilt_command=OverkizCommand.TILT_DOWN, + stop_tilt_command=OverkizCommand.STOP, + ), + OverkizCoverDescription( + key=UIClass.WINDOW, + device_class=CoverDeviceClass.WINDOW, + current_position_state=OverkizState.CORE_CLOSURE, + set_position_command=OverkizCommand.SET_CLOSURE, + open_command=OverkizCommand.OPEN, + close_command=OverkizCommand.CLOSE, + is_closed_state=OverkizState.CORE_OPEN_CLOSED, + stop_command=OverkizCommand.STOP, + ), +] + +SUPPORTED_DEVICES = {description.key: description for description in COVER_DESCRIPTIONS} + + +async def async_setup_entry( + hass: HomeAssistant, + entry: OverkizDataConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up the Overkiz covers from a config entry.""" + data = entry.runtime_data + + entities: list[OverkizCover] = [] + + for device in data.platforms[Platform.COVER]: + if description := ( + SUPPORTED_DEVICES.get(device.widget) + or SUPPORTED_DEVICES.get(device.ui_class) + ): + entities.append( + OverkizCover(device.device_url, data.coordinator, description) + ) + + # Cover platform does not support configuring the speed of the cover + # For covers where the speed can be configured, we create a separate entity + if ( + OverkizCommand.SET_CLOSURE_AND_LINEAR_SPEED + in device.definition.commands + ): + entities.append( + OverkizLowSpeedCover( + device.device_url, data.coordinator, description + ) + ) + + async_add_entities(entities) + + +class OverkizCover(OverkizDescriptiveEntity, CoverEntity): + """Representation of an Overkiz Cover.""" + + entity_description: OverkizCoverDescription + + def __init__( + self, + device_url: str, + coordinator: OverkizDataUpdateCoordinator, + description: OverkizCoverDescription, + ) -> None: + """Initialize the device.""" + super().__init__(device_url, coordinator, description) + + # Use device url as unique ID for backwards compatibility + self._attr_unique_id = self.device.device_url + + # Overkiz does support covers where only tilt commands are supported + # and HA sets by default open/close as supported feature which conflicts + supported_features = CoverEntityFeature(0) + + if self.entity_description.open_command and self.executor.has_command( + self.entity_description.open_command + ): + supported_features |= CoverEntityFeature.OPEN + + if self.entity_description.stop_command and self.executor.has_command( + self.entity_description.stop_command + ): + supported_features |= CoverEntityFeature.STOP + + if self.entity_description.close_command and self.executor.has_command( + self.entity_description.close_command + ): + supported_features |= CoverEntityFeature.CLOSE + + if self.entity_description.open_tilt_command and self.executor.has_command( + self.entity_description.open_tilt_command + ): + supported_features |= CoverEntityFeature.OPEN_TILT + + if self.entity_description.stop_tilt_command and self.executor.has_command( + self.entity_description.stop_tilt_command + ): + supported_features |= CoverEntityFeature.STOP_TILT + + if self.entity_description.close_tilt_command and self.executor.has_command( + self.entity_description.close_tilt_command + ): + supported_features |= CoverEntityFeature.CLOSE_TILT + + if ( + self.entity_description.set_tilt_position_command + and self.executor.has_command( + self.entity_description.set_tilt_position_command + ) + ): + supported_features |= CoverEntityFeature.SET_TILT_POSITION + + if self.entity_description.set_position_command and self.executor.has_command( + self.entity_description.set_position_command + ): + supported_features |= CoverEntityFeature.SET_POSITION + + self._attr_supported_features = supported_features + + @property + def is_closed(self) -> bool | None: + """Return if the cover is closed.""" + if is_closed_state := self.entity_description.is_closed_state: + if state := self.device.states.get(is_closed_state): + return state.value == OverkizCommandParam.CLOSED + + if (position := self.current_cover_position) is not None: + return position == 0 + + if (tilt_position := self.current_cover_tilt_position) is not None: + return tilt_position == 0 + + return None + + @property + def current_cover_position(self) -> int | None: + """Return current position of cover. + + None is unknown, 0 is closed, 100 is fully open. + """ + state_name = self.entity_description.current_position_state + + if not state_name or not (state := self.device.states[state_name]): + return None + + position = state.value_as_int + + # Fallback for "My position" preset + if position == _POSITION_MY: + LOGGER.debug( + "Overkiz cover position is invalid (%s). Device: %s, State: %s", + _POSITION_MY, + self.device.device_url, + state_name, + ) + + if fallback_state := self.device.states[ + OverkizState.CORE_MEMORIZED_1_POSITION + ]: + position = fallback_state.value_as_int + else: + return None + + # Fallback for "Unknown position" preset + if position == _POSITION_UNKNOWN: + LOGGER.debug( + "Overkiz cover position is invalid (%s). Device: %s, State: %s", + _POSITION_UNKNOWN, + self.device.device_url, + state_name, + ) + + if fallback_state := self.device.states[OverkizState.CORE_TARGET_CLOSURE]: + position = fallback_state.value_as_int + else: + return None + + if position is None: + return None + + # Invert position if needed (some devices report 0 as open and 100 as closed) + if self.entity_description.invert_position: + position = 100 - position + + return position + + async def async_set_cover_position(self, **kwargs: Any) -> None: + """Move the cover to a specific position.""" + position = kwargs[ATTR_POSITION] + if self.entity_description.invert_position: + position = 100 - position + + if command := self.entity_description.set_position_command: + await self.executor.async_execute_command(command, position) + + async def async_open_cover(self, **kwargs: Any) -> None: + """Open the cover.""" + if command := self.entity_description.open_command: + await self.executor.async_execute_command(command) + + async def async_close_cover(self, **kwargs: Any) -> None: + """Close the cover.""" + if command := self.entity_description.close_command: + await self.executor.async_execute_command(command) + + async def async_stop_cover(self, **kwargs: Any) -> None: + """Stop the cover.""" + if command := self.entity_description.stop_command: + await self.executor.async_execute_command(command) + + @property + def current_cover_tilt_position(self) -> int | None: + """Return current position of cover tilt. + + None is unknown, 0 is closed, 100 is fully open. + """ + state_name = self.entity_description.current_tilt_position_state + + if state_name and (state := self.device.states[state_name]): + position = state.value_as_int + if position is None: + return None + + if self.entity_description.invert_tilt_position: + position = 100 - position + + return position + + return None + + async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: + """Move the cover tilt to a specific position.""" + position = kwargs[ATTR_TILT_POSITION] + + if self.entity_description.invert_tilt_position: + position = 100 - position + + if command := self.entity_description.set_tilt_position_command: + await self.executor.async_execute_command(command, position) + + async def async_open_cover_tilt(self, **kwargs: Any) -> None: + """Open the cover tilt.""" + if command := self.entity_description.open_tilt_command: + await self.executor.async_execute_command( + command, *self.entity_description.open_tilt_command_args + ) + + async def async_close_cover_tilt(self, **kwargs: Any) -> None: + """Close the cover tilt.""" + if command := self.entity_description.close_tilt_command: + await self.executor.async_execute_command( + command, *self.entity_description.close_tilt_command_args + ) + + async def async_stop_cover_tilt(self, **kwargs: Any) -> None: + """Stop the cover tilt.""" + if command := self.entity_description.stop_tilt_command: + await self.executor.async_execute_command(command) + + @property + def is_opening(self) -> bool | None: + """Return if the cover is opening or not.""" + # Check if any open() commands are currently running for this device + if (command := self.entity_description.open_command) and self.is_running( + command + ): + return True + + # Check if any open_tilt() commands are currently running for this device + if (command := self.entity_description.open_tilt_command) and self.is_running( + command + ): + return True + + if self.moving_offset is None: + return None + + # Check if the cover is moving in a direction consistent with opening + if self.entity_description.invert_position: + return self.moving_offset > 0 + return self.moving_offset < 0 + + @property + def is_closing(self) -> bool | None: + """Return if the cover is closing or not.""" + # Check if any close() commands are currently running for this device + if (command := self.entity_description.close_command) and self.is_running( + command + ): + return True + + # Check if any close_tilt() commands are currently running for this device + if (command := self.entity_description.close_tilt_command) and self.is_running( + command + ): + return True + + if self.moving_offset is None: + return None + + # Check if the cover is moving in a direction consistent with closing + if self.entity_description.invert_position: + return self.moving_offset < 0 + return self.moving_offset > 0 + + def is_running(self, command: OverkizCommand) -> bool: + """Return if the given commands are currently running.""" + return any( + execution.get("device_url") == self.device.device_url + and execution.get("command_name") == command + for execution in self.coordinator.executions.values() + ) + + @property + def moving_offset(self) -> int | None: + """Return the offset between the targeted position and the current one if the cover is moving.""" + moving_state = self.device.states.get(OverkizState.CORE_MOVING) + if moving_state is None or moving_state.value_as_bool is not True: + return None + + current_closure = self.device.states.get( + self.entity_description.current_position_state or OverkizState.CORE_CLOSURE + ) + target_closure = self.device.states.get(OverkizState.CORE_TARGET_CLOSURE) + + if not current_closure or not target_closure: + return None + + current_value = current_closure.value_as_int + target_value = target_closure.value_as_int + + if current_value is None or target_value is None: + return None + + return current_value - target_value + + +class OverkizLowSpeedCover(OverkizCover): + """Representation of an Overkiz Low Speed cover.""" + + def __init__( + self, + device_url: str, + coordinator: OverkizDataUpdateCoordinator, + description: OverkizCoverDescription, + ) -> None: + """Initialize the device.""" + super().__init__(device_url, coordinator, description) + + self._attr_name = "Low speed" + self._attr_unique_id = f"{self._attr_unique_id}_low_speed" + + async def async_set_cover_position(self, **kwargs: Any) -> None: + """Move the cover to a specific position.""" + await self._async_set_cover_position_low_speed(kwargs[ATTR_POSITION]) + + async def async_open_cover(self, **kwargs: Any) -> None: + """Open the cover.""" + await self._async_set_cover_position_low_speed(100) + + async def async_close_cover(self, **kwargs: Any) -> None: + """Close the cover.""" + await self._async_set_cover_position_low_speed(0) + + async def _async_set_cover_position_low_speed(self, position: int) -> None: + """Move the cover to a specific position with a low speed.""" + await self.executor.async_execute_command( + OverkizCommand.SET_CLOSURE_AND_LINEAR_SPEED, + 100 - position, + OverkizCommandParam.LOWSPEED, + ) diff --git a/homeassistant/components/overkiz/cover/__init__.py b/homeassistant/components/overkiz/cover/__init__.py deleted file mode 100644 index dd3216f9c1095..0000000000000 --- a/homeassistant/components/overkiz/cover/__init__.py +++ /dev/null @@ -1,41 +0,0 @@ -"""Support for Overkiz covers - shutters etc.""" - -from pyoverkiz.enums import OverkizCommand, UIClass - -from homeassistant.const import Platform -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback - -from .. import OverkizDataConfigEntry -from .awning import Awning -from .generic_cover import OverkizGenericCover -from .vertical_cover import LowSpeedCover, VerticalCover - - -async def async_setup_entry( - hass: HomeAssistant, - entry: OverkizDataConfigEntry, - async_add_entities: AddConfigEntryEntitiesCallback, -) -> None: - """Set up the Overkiz covers from a config entry.""" - data = entry.runtime_data - - entities: list[OverkizGenericCover] = [ - Awning(device.device_url, data.coordinator) - for device in data.platforms[Platform.COVER] - if device.ui_class == UIClass.AWNING - ] - - entities += [ - VerticalCover(device.device_url, data.coordinator) - for device in data.platforms[Platform.COVER] - if device.ui_class != UIClass.AWNING - ] - - entities += [ - LowSpeedCover(device.device_url, data.coordinator) - for device in data.platforms[Platform.COVER] - if OverkizCommand.SET_CLOSURE_AND_LINEAR_SPEED in device.definition.commands - ] - - async_add_entities(entities) diff --git a/homeassistant/components/overkiz/cover/awning.py b/homeassistant/components/overkiz/cover/awning.py deleted file mode 100644 index 4b6e5b176a757..0000000000000 --- a/homeassistant/components/overkiz/cover/awning.py +++ /dev/null @@ -1,103 +0,0 @@ -"""Support for Overkiz awnings.""" - -from __future__ import annotations - -from typing import Any, cast - -from pyoverkiz.enums import OverkizCommand, OverkizState - -from homeassistant.components.cover import ( - ATTR_POSITION, - CoverDeviceClass, - CoverEntityFeature, -) - -from .generic_cover import ( - COMMANDS_CLOSE, - COMMANDS_OPEN, - COMMANDS_STOP, - OverkizGenericCover, -) - - -class Awning(OverkizGenericCover): - """Representation of an Overkiz awning.""" - - _attr_device_class = CoverDeviceClass.AWNING - - @property - def supported_features(self) -> CoverEntityFeature: - """Flag supported features.""" - supported_features = super().supported_features - - if self.executor.has_command(OverkizCommand.SET_DEPLOYMENT): - supported_features |= CoverEntityFeature.SET_POSITION - - if self.executor.has_command(OverkizCommand.DEPLOY): - supported_features |= CoverEntityFeature.OPEN - - if self.executor.has_command(*COMMANDS_STOP): - supported_features |= CoverEntityFeature.STOP - - if self.executor.has_command(OverkizCommand.UNDEPLOY): - supported_features |= CoverEntityFeature.CLOSE - - return supported_features - - @property - def current_cover_position(self) -> int | None: - """Return current position of cover. - - None is unknown, 0 is closed, 100 is fully open. - """ - current_position = self.executor.select_state(OverkizState.CORE_DEPLOYMENT) - if current_position is not None: - return cast(int, current_position) - - return None - - async def async_set_cover_position(self, **kwargs: Any) -> None: - """Move the cover to a specific position.""" - await self.executor.async_execute_command( - OverkizCommand.SET_DEPLOYMENT, kwargs[ATTR_POSITION] - ) - - async def async_open_cover(self, **kwargs: Any) -> None: - """Open the cover.""" - await self.executor.async_execute_command(OverkizCommand.DEPLOY) - - async def async_close_cover(self, **kwargs: Any) -> None: - """Close the cover.""" - await self.executor.async_execute_command(OverkizCommand.UNDEPLOY) - - @property - def is_opening(self) -> bool | None: - """Return if the cover is opening or not.""" - if self.is_running(COMMANDS_OPEN): - return True - - # Check if cover is moving based on current state - is_moving = self.device.states.get(OverkizState.CORE_MOVING) - current_closure = self.device.states.get(OverkizState.CORE_DEPLOYMENT) - target_closure = self.device.states.get(OverkizState.CORE_TARGET_CLOSURE) - - if not is_moving or not current_closure or not target_closure: - return None - - return cast(int, current_closure.value) < cast(int, target_closure.value) - - @property - def is_closing(self) -> bool | None: - """Return if the cover is closing or not.""" - if self.is_running(COMMANDS_CLOSE): - return True - - # Check if cover is moving based on current state - is_moving = self.device.states.get(OverkizState.CORE_MOVING) - current_closure = self.device.states.get(OverkizState.CORE_DEPLOYMENT) - target_closure = self.device.states.get(OverkizState.CORE_TARGET_CLOSURE) - - if not is_moving or not current_closure or not target_closure: - return None - - return cast(int, current_closure.value) > cast(int, target_closure.value) diff --git a/homeassistant/components/overkiz/cover/generic_cover.py b/homeassistant/components/overkiz/cover/generic_cover.py deleted file mode 100644 index df13072524d06..0000000000000 --- a/homeassistant/components/overkiz/cover/generic_cover.py +++ /dev/null @@ -1,141 +0,0 @@ -"""Base class for Overkiz covers, shutters, awnings, etc.""" - -from __future__ import annotations - -from typing import Any, cast - -from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState - -from homeassistant.components.cover import ( - ATTR_TILT_POSITION, - CoverEntity, - CoverEntityFeature, -) - -from ..entity import OverkizEntity - -ATTR_OBSTRUCTION_DETECTED = "obstruction-detected" - -COMMANDS_STOP: list[OverkizCommand] = [ - OverkizCommand.STOP, - OverkizCommand.MY, -] -COMMANDS_STOP_TILT: list[OverkizCommand] = [ - OverkizCommand.STOP, - OverkizCommand.MY, -] -COMMANDS_OPEN: list[OverkizCommand] = [ - OverkizCommand.OPEN, - OverkizCommand.UP, -] -COMMANDS_OPEN_TILT: list[OverkizCommand] = [ - OverkizCommand.OPEN_SLATS, - OverkizCommand.TILT_DOWN, -] -COMMANDS_CLOSE: list[OverkizCommand] = [ - OverkizCommand.CLOSE, - OverkizCommand.DOWN, -] -COMMANDS_CLOSE_TILT: list[OverkizCommand] = [ - OverkizCommand.CLOSE_SLATS, - OverkizCommand.TILT_UP, -] - -COMMANDS_SET_TILT_POSITION: list[OverkizCommand] = [OverkizCommand.SET_ORIENTATION] - - -class OverkizGenericCover(OverkizEntity, CoverEntity): - """Representation of an Overkiz Cover.""" - - @property - def current_cover_tilt_position(self) -> int | None: - """Return current position of cover tilt. - - None is unknown, 0 is closed, 100 is fully open. - """ - position = self.executor.select_state( - OverkizState.CORE_SLATS_ORIENTATION, OverkizState.CORE_SLATE_ORIENTATION - ) - if position is not None: - return 100 - cast(int, position) - - return None - - async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: - """Move the cover tilt to a specific position.""" - if command := self.executor.select_command(*COMMANDS_SET_TILT_POSITION): - await self.executor.async_execute_command( - command, - 100 - kwargs[ATTR_TILT_POSITION], - ) - - @property - def is_closed(self) -> bool | None: - """Return if the cover is closed.""" - - state = self.executor.select_state( - OverkizState.CORE_OPEN_CLOSED, - OverkizState.CORE_SLATS_OPEN_CLOSED, - OverkizState.CORE_OPEN_CLOSED_PARTIAL, - OverkizState.CORE_OPEN_CLOSED_PEDESTRIAN, - OverkizState.CORE_OPEN_CLOSED_UNKNOWN, - OverkizState.MYFOX_SHUTTER_STATUS, - ) - if state is not None: - return state == OverkizCommandParam.CLOSED - - # Keep this condition after the previous one. Some device like the pedestrian gate, always return 50 as position. - if self.current_cover_position is not None: - return self.current_cover_position == 0 - - if self.current_cover_tilt_position is not None: - return self.current_cover_tilt_position == 0 - - return None - - async def async_open_cover_tilt(self, **kwargs: Any) -> None: - """Open the cover tilt.""" - if command := self.executor.select_command(*COMMANDS_OPEN_TILT): - await self.executor.async_execute_command(command) - - async def async_close_cover_tilt(self, **kwargs: Any) -> None: - """Close the cover tilt.""" - if command := self.executor.select_command(*COMMANDS_CLOSE_TILT): - await self.executor.async_execute_command(command) - - async def async_stop_cover(self, **kwargs: Any) -> None: - """Stop the cover.""" - if command := self.executor.select_command(*COMMANDS_STOP): - await self.executor.async_execute_command(command) - - async def async_stop_cover_tilt(self, **kwargs: Any) -> None: - """Stop the cover tilt.""" - if command := self.executor.select_command(*COMMANDS_STOP_TILT): - await self.executor.async_execute_command(command) - - def is_running(self, commands: list[OverkizCommand]) -> bool: - """Return if the given commands are currently running.""" - return any( - execution.get("device_url") == self.device.device_url - and execution.get("command_name") in commands - for execution in self.coordinator.executions.values() - ) - - @property - def supported_features(self) -> CoverEntityFeature: - """Flag supported features.""" - supported_features = CoverEntityFeature(0) - - if self.executor.has_command(*COMMANDS_OPEN_TILT): - supported_features |= CoverEntityFeature.OPEN_TILT - - if self.executor.has_command(*COMMANDS_STOP_TILT): - supported_features |= CoverEntityFeature.STOP_TILT - - if self.executor.has_command(*COMMANDS_CLOSE_TILT): - supported_features |= CoverEntityFeature.CLOSE_TILT - - if self.executor.has_command(*COMMANDS_SET_TILT_POSITION): - supported_features |= CoverEntityFeature.SET_TILT_POSITION - - return supported_features diff --git a/homeassistant/components/overkiz/cover/vertical_cover.py b/homeassistant/components/overkiz/cover/vertical_cover.py deleted file mode 100644 index 48ac2c838c535..0000000000000 --- a/homeassistant/components/overkiz/cover/vertical_cover.py +++ /dev/null @@ -1,177 +0,0 @@ -"""Support for Overkiz Vertical Covers.""" - -from __future__ import annotations - -from typing import Any, cast - -from pyoverkiz.enums import ( - OverkizCommand, - OverkizCommandParam, - OverkizState, - UIClass, - UIWidget, -) - -from homeassistant.components.cover import ( - ATTR_POSITION, - CoverDeviceClass, - CoverEntityFeature, -) - -from ..coordinator import OverkizDataUpdateCoordinator -from .generic_cover import ( - COMMANDS_CLOSE_TILT, - COMMANDS_OPEN_TILT, - COMMANDS_STOP, - OverkizGenericCover, -) - -COMMANDS_OPEN = [OverkizCommand.OPEN, OverkizCommand.UP, OverkizCommand.CYCLE] -COMMANDS_CLOSE = [OverkizCommand.CLOSE, OverkizCommand.DOWN, OverkizCommand.CYCLE] - -OVERKIZ_DEVICE_TO_DEVICE_CLASS = { - UIClass.CURTAIN: CoverDeviceClass.CURTAIN, - UIClass.EXTERIOR_SCREEN: CoverDeviceClass.BLIND, - UIClass.EXTERIOR_VENETIAN_BLIND: CoverDeviceClass.BLIND, - UIClass.GARAGE_DOOR: CoverDeviceClass.GARAGE, - UIClass.GATE: CoverDeviceClass.GATE, - UIWidget.MY_FOX_SECURITY_CAMERA: CoverDeviceClass.SHUTTER, - UIClass.PERGOLA: CoverDeviceClass.AWNING, - UIClass.ROLLER_SHUTTER: CoverDeviceClass.SHUTTER, - UIClass.SWINGING_SHUTTER: CoverDeviceClass.SHUTTER, - UIClass.WINDOW: CoverDeviceClass.WINDOW, -} - - -class VerticalCover(OverkizGenericCover): - """Representation of an Overkiz vertical cover.""" - - def __init__( - self, device_url: str, coordinator: OverkizDataUpdateCoordinator - ) -> None: - """Initialize vertical cover.""" - super().__init__(device_url, coordinator) - self._attr_device_class = ( - OVERKIZ_DEVICE_TO_DEVICE_CLASS.get(self.device.widget) - or OVERKIZ_DEVICE_TO_DEVICE_CLASS.get(self.device.ui_class) - or CoverDeviceClass.BLIND - ) - - @property - def supported_features(self) -> CoverEntityFeature: - """Flag supported features.""" - supported_features = super().supported_features - - if self.executor.has_command(OverkizCommand.SET_CLOSURE): - supported_features |= CoverEntityFeature.SET_POSITION - - if self.executor.has_command(*COMMANDS_OPEN): - supported_features |= CoverEntityFeature.OPEN - - if self.executor.has_command(*COMMANDS_STOP): - supported_features |= CoverEntityFeature.STOP - - if self.executor.has_command(*COMMANDS_CLOSE): - supported_features |= CoverEntityFeature.CLOSE - - return supported_features - - @property - def current_cover_position(self) -> int | None: - """Return current position of cover. - - None is unknown, 0 is closed, 100 is fully open. - """ - position = self.executor.select_state( - OverkizState.CORE_CLOSURE, - OverkizState.CORE_CLOSURE_OR_ROCKER_POSITION, - OverkizState.CORE_PEDESTRIAN_POSITION, - ) - - if position is None: - return None - - return 100 - cast(int, position) - - async def async_set_cover_position(self, **kwargs: Any) -> None: - """Move the cover to a specific position.""" - position = 100 - kwargs[ATTR_POSITION] - await self.executor.async_execute_command(OverkizCommand.SET_CLOSURE, position) - - async def async_open_cover(self, **kwargs: Any) -> None: - """Open the cover.""" - if command := self.executor.select_command(*COMMANDS_OPEN): - await self.executor.async_execute_command(command) - - async def async_close_cover(self, **kwargs: Any) -> None: - """Close the cover.""" - if command := self.executor.select_command(*COMMANDS_CLOSE): - await self.executor.async_execute_command(command) - - @property - def is_opening(self) -> bool | None: - """Return if the cover is opening or not.""" - if self.is_running(COMMANDS_OPEN + COMMANDS_OPEN_TILT): - return True - - # Check if cover is moving based on current state - is_moving = self.device.states.get(OverkizState.CORE_MOVING) - current_closure = self.device.states.get(OverkizState.CORE_CLOSURE) - target_closure = self.device.states.get(OverkizState.CORE_TARGET_CLOSURE) - - if not is_moving or not current_closure or not target_closure: - return None - - return cast(int, current_closure.value) > cast(int, target_closure.value) - - @property - def is_closing(self) -> bool | None: - """Return if the cover is closing or not.""" - if self.is_running(COMMANDS_CLOSE + COMMANDS_CLOSE_TILT): - return True - - # Check if cover is moving based on current state - is_moving = self.device.states.get(OverkizState.CORE_MOVING) - current_closure = self.device.states.get(OverkizState.CORE_CLOSURE) - target_closure = self.device.states.get(OverkizState.CORE_TARGET_CLOSURE) - - if not is_moving or not current_closure or not target_closure: - return None - - return cast(int, current_closure.value) < cast(int, target_closure.value) - - -class LowSpeedCover(VerticalCover): - """Representation of an Overkiz Low Speed cover.""" - - def __init__( - self, - device_url: str, - coordinator: OverkizDataUpdateCoordinator, - ) -> None: - """Initialize the device.""" - super().__init__(device_url, coordinator) - self._attr_name = "Low speed" - self._attr_unique_id = f"{self._attr_unique_id}_low_speed" - - async def async_set_cover_position(self, **kwargs: Any) -> None: - """Move the cover to a specific position.""" - await self.async_set_cover_position_low_speed(**kwargs) - - async def async_open_cover(self, **kwargs: Any) -> None: - """Open the cover.""" - await self.async_set_cover_position_low_speed(**{ATTR_POSITION: 100}) - - async def async_close_cover(self, **kwargs: Any) -> None: - """Close the cover.""" - await self.async_set_cover_position_low_speed(**{ATTR_POSITION: 0}) - - async def async_set_cover_position_low_speed(self, **kwargs: Any) -> None: - """Move the cover to a specific position with a low speed.""" - position = 100 - kwargs.get(ATTR_POSITION, 0) - - await self.executor.async_execute_command( - OverkizCommand.SET_CLOSURE_AND_LINEAR_SPEED, - position, - OverkizCommandParam.LOWSPEED, - ) diff --git a/tests/components/overkiz/snapshots/test_cover.ambr b/tests/components/overkiz/snapshots/test_cover.ambr index ba0a541e75a89..dc35a2d803bc9 100644 --- a/tests/components/overkiz/snapshots/test_cover.ambr +++ b/tests/components/overkiz/snapshots/test_cover.ambr @@ -462,7 +462,7 @@ 'platform': 'overkiz', 'previous_unique_id': None, 'suggested_object_id': None, - 'supported_features': , + 'supported_features': , 'translation_key': None, 'unique_id': 'io://1234-5678-5010/2150846', 'unit_of_measurement': None, @@ -471,11 +471,12 @@ # name: test_cover_entities_snapshot[cloud_somfy_tahoma_switch_sc_europe.json][cover.basement_roller_shutter-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'current_position': 90, + 'current_position': 80, + 'current_tilt_position': 100, 'device_class': 'shutter', 'friendly_name': 'Basement Roller Shutter', 'is_closed': False, - 'supported_features': , + 'supported_features': , }), 'context': , 'entity_id': 'cover.basement_roller_shutter', @@ -840,7 +841,7 @@ 'platform': 'overkiz', 'previous_unique_id': None, 'suggested_object_id': None, - 'supported_features': , + 'supported_features': , 'translation_key': None, 'unique_id': 'io://1234-5678-5010/12931361', 'unit_of_measurement': None, @@ -849,18 +850,19 @@ # name: test_cover_entities_snapshot[cloud_somfy_tahoma_switch_sc_europe.json][cover.veranda_roller_shutter-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'current_position': 50, + 'current_position': 0, + 'current_tilt_position': 100, 'device_class': 'shutter', 'friendly_name': 'Veranda Roller Shutter', - 'is_closed': False, - 'supported_features': , + 'is_closed': True, + 'supported_features': , }), 'context': , 'entity_id': 'cover.veranda_roller_shutter', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'open', + 'state': 'closed', }) # --- # name: test_cover_entities_snapshot[cloud_somfy_tahoma_switch_sc_europe.json][cover.workshop_screen-entry] @@ -1119,7 +1121,6 @@ # name: test_cover_entities_snapshot[cloud_somfy_tahoma_v2_europe.json][cover.garden_gate-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'current_position': 50, 'device_class': 'gate', 'friendly_name': 'Garden Gate', 'is_closed': True, @@ -1767,7 +1768,7 @@ # name: test_cover_entities_snapshot[local_somfy_tahoma_switch_europe.json][cover.bathroom_blinds-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'current_position': -8, + 'current_position': 0, 'current_tilt_position': 68, 'device_class': 'blind', 'friendly_name': 'Bathroom Blinds', @@ -1779,7 +1780,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'opening', + 'state': 'open', }) # --- # name: test_cover_entities_snapshot[local_somfy_tahoma_switch_europe.json][cover.bedroom_blinds-entry] @@ -1822,7 +1823,7 @@ # name: test_cover_entities_snapshot[local_somfy_tahoma_switch_europe.json][cover.bedroom_blinds-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'current_position': -8, + 'current_position': 0, 'current_tilt_position': 68, 'device_class': 'blind', 'friendly_name': 'Bedroom Blinds', @@ -1834,7 +1835,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'opening', + 'state': 'open', }) # --- # name: test_cover_entities_snapshot[local_somfy_tahoma_switch_europe.json][cover.dining_room_blinds-entry] @@ -1932,7 +1933,7 @@ # name: test_cover_entities_snapshot[local_somfy_tahoma_switch_europe.json][cover.garage_blinds-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'current_position': -8, + 'current_position': 0, 'current_tilt_position': 69, 'device_class': 'blind', 'friendly_name': 'Garage Blinds', @@ -1944,7 +1945,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'opening', + 'state': 'open', }) # --- # name: test_cover_entities_snapshot[local_somfy_tahoma_switch_europe.json][cover.guest_room_blinds-entry] @@ -1987,7 +1988,7 @@ # name: test_cover_entities_snapshot[local_somfy_tahoma_switch_europe.json][cover.guest_room_blinds-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'current_position': -8, + 'current_position': 0, 'current_tilt_position': 68, 'device_class': 'blind', 'friendly_name': 'Guest Room Blinds', @@ -1999,7 +2000,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'opening', + 'state': 'open', }) # --- # name: test_cover_entities_snapshot[local_somfy_tahoma_switch_europe.json][cover.hallway_blinds-entry] @@ -2042,7 +2043,7 @@ # name: test_cover_entities_snapshot[local_somfy_tahoma_switch_europe.json][cover.hallway_blinds-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'current_position': -8, + 'current_position': 0, 'current_tilt_position': 68, 'device_class': 'blind', 'friendly_name': 'Hallway Blinds', @@ -2054,7 +2055,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'opening', + 'state': 'open', }) # --- # name: test_cover_entities_snapshot[local_somfy_tahoma_switch_europe.json][cover.kitchen_blinds-entry] @@ -2097,7 +2098,7 @@ # name: test_cover_entities_snapshot[local_somfy_tahoma_switch_europe.json][cover.kitchen_blinds-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'current_position': -8, + 'current_position': 0, 'current_tilt_position': 68, 'device_class': 'blind', 'friendly_name': 'Kitchen Blinds', @@ -2109,7 +2110,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'opening', + 'state': 'open', }) # --- # name: test_cover_entities_snapshot[local_somfy_tahoma_switch_europe.json][cover.living_room_blinds-entry] @@ -2152,7 +2153,7 @@ # name: test_cover_entities_snapshot[local_somfy_tahoma_switch_europe.json][cover.living_room_blinds-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'current_position': -8, + 'current_position': 0, 'current_tilt_position': 68, 'device_class': 'blind', 'friendly_name': 'Living Room Blinds', @@ -2164,7 +2165,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'opening', + 'state': 'open', }) # --- # name: test_cover_entities_snapshot[local_somfy_tahoma_switch_europe.json][cover.master_bedroom_blinds-entry] @@ -2207,7 +2208,7 @@ # name: test_cover_entities_snapshot[local_somfy_tahoma_switch_europe.json][cover.master_bedroom_blinds-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'current_position': -8, + 'current_position': 0, 'current_tilt_position': 68, 'device_class': 'blind', 'friendly_name': 'Master Bedroom Blinds', @@ -2219,7 +2220,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'opening', + 'state': 'open', }) # --- # name: test_cover_entities_snapshot[local_somfy_tahoma_switch_europe.json][cover.nursery_blinds-entry] @@ -2262,7 +2263,7 @@ # name: test_cover_entities_snapshot[local_somfy_tahoma_switch_europe.json][cover.nursery_blinds-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'current_position': -8, + 'current_position': 0, 'current_tilt_position': 69, 'device_class': 'blind', 'friendly_name': 'Nursery Blinds', @@ -2274,7 +2275,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'opening', + 'state': 'open', }) # --- # name: test_cover_entities_snapshot[local_somfy_tahoma_switch_europe.json][cover.office_blinds-entry] @@ -2317,7 +2318,7 @@ # name: test_cover_entities_snapshot[local_somfy_tahoma_switch_europe.json][cover.office_blinds-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'current_position': -8, + 'current_position': 0, 'current_tilt_position': 68, 'device_class': 'blind', 'friendly_name': 'Office Blinds', @@ -2329,7 +2330,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'opening', + 'state': 'open', }) # --- # name: test_cover_entities_snapshot[local_somfy_tahoma_switch_europe.json][cover.study_blinds-entry] @@ -2372,7 +2373,7 @@ # name: test_cover_entities_snapshot[local_somfy_tahoma_switch_europe.json][cover.study_blinds-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'current_position': -8, + 'current_position': 0, 'current_tilt_position': 72, 'device_class': 'blind', 'friendly_name': 'Study Blinds', @@ -2384,7 +2385,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'opening', + 'state': 'open', }) # --- # name: test_cover_entities_snapshot[local_somfy_tahoma_switch_europe_2.json][cover.back_door_shutter-entry] @@ -2427,7 +2428,7 @@ # name: test_cover_entities_snapshot[local_somfy_tahoma_switch_europe_2.json][cover.back_door_shutter-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'current_position': -24, + 'current_position': 100, 'device_class': 'shutter', 'friendly_name': 'Back Door Shutter', 'is_closed': False, @@ -2438,7 +2439,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'opening', + 'state': 'open', }) # --- # name: test_cover_entities_snapshot[local_somfy_tahoma_switch_europe_2.json][cover.front_door_shutter-entry] @@ -2481,7 +2482,7 @@ # name: test_cover_entities_snapshot[local_somfy_tahoma_switch_europe_2.json][cover.front_door_shutter-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'current_position': -24, + 'current_position': 100, 'device_class': 'shutter', 'friendly_name': 'Front Door Shutter', 'is_closed': False, @@ -2492,7 +2493,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'opening', + 'state': 'open', }) # --- # name: test_cover_entities_snapshot[local_somfy_tahoma_switch_europe_2.json][cover.roof_window-entry] diff --git a/tests/components/overkiz/test_cover.py b/tests/components/overkiz/test_cover.py index 061d7b64f09cc..caaf0dfdce31c 100644 --- a/tests/components/overkiz/test_cover.py +++ b/tests/components/overkiz/test_cover.py @@ -133,24 +133,10 @@ async def test_cover_entities_snapshot( ("device", "service", "command_name", "expected_state"), [ (SHUTTER, SERVICE_OPEN_COVER, "open", CoverState.OPENING), - pytest.param( - AWNING, - SERVICE_OPEN_COVER, - "deploy", - CoverState.OPENING, - marks=pytest.mark.xfail(reason="Awning deploy not mapped to opening state"), - ), + (AWNING, SERVICE_OPEN_COVER, "deploy", CoverState.OPENING), (GARAGE, SERVICE_OPEN_COVER, "open", CoverState.OPENING), (SHUTTER, SERVICE_CLOSE_COVER, "close", CoverState.CLOSING), - pytest.param( - AWNING, - SERVICE_CLOSE_COVER, - "undeploy", - CoverState.CLOSING, - marks=pytest.mark.xfail( - reason="Awning undeploy not mapped to closing state" - ), - ), + (AWNING, SERVICE_CLOSE_COVER, "undeploy", CoverState.CLOSING), (GARAGE, SERVICE_CLOSE_COVER, "close", CoverState.CLOSING), (SHUTTER, SERVICE_STOP_COVER, "stop", CoverState.CLOSED), (AWNING, SERVICE_STOP_COVER, "stop", CoverState.CLOSED), @@ -668,3 +654,151 @@ async def test_awning_direct_position_mapping( ], ) assert hass.states.get(AWNING.entity_id).attributes[ATTR_CURRENT_POSITION] == 100 + + +async def test_moving_offset_missing_closure_states( + hass: HomeAssistant, + setup_overkiz_integration: SetupOverkizIntegration, + mock_client: MockOverkizClient, + freezer: FrozenDateTimeFactory, +) -> None: + """Test that is_opening/is_closing return None when closure states are missing while moving.""" + await setup_overkiz_integration(fixture=PERGOLA.fixture) + + await async_deliver_events( + hass, + freezer, + mock_client, + [ + build_event( + EventName.DEVICE_STATE_CHANGED.value, + device_url=PERGOLA.device_url, + device_states=[ + { + "name": OverkizState.CORE_MOVING.value, + "type": 6, + "value": True, + }, + ], + ) + ], + ) + + state = hass.states.get(PERGOLA.entity_id) + assert state.state == CoverState.CLOSED + + +async def test_moving_offset_none_values( + hass: HomeAssistant, + setup_overkiz_integration: SetupOverkizIntegration, + mock_client: MockOverkizClient, + freezer: FrozenDateTimeFactory, +) -> None: + """Test that is_opening/is_closing return None when closure value_as_int is None.""" + await setup_overkiz_integration(fixture=SHUTTER.fixture) + + await async_deliver_events( + hass, + freezer, + mock_client, + [ + build_event( + EventName.DEVICE_STATE_CHANGED.value, + device_url=SHUTTER.device_url, + device_states=[ + { + "name": OverkizState.CORE_MOVING.value, + "type": 6, + "value": True, + }, + { + "name": OverkizState.CORE_CLOSURE.value, + "type": 1, + "value": None, + }, + { + "name": OverkizState.CORE_TARGET_CLOSURE.value, + "type": 1, + "value": 50, + }, + { + "name": OverkizState.CORE_OPEN_CLOSED.value, + "type": 3, + "value": OverkizCommandParam.OPEN.value, + }, + ], + ) + ], + ) + + state = hass.states.get(SHUTTER.entity_id) + assert state.state == CoverState.OPEN + + +async def test_tilt_position_none_value( + hass: HomeAssistant, + setup_overkiz_integration: SetupOverkizIntegration, + mock_client: MockOverkizClient, + freezer: FrozenDateTimeFactory, +) -> None: + """Test that tilt position returns None when value_as_int is None.""" + await setup_overkiz_integration(fixture=PERGOLA.fixture) + + await async_deliver_events( + hass, + freezer, + mock_client, + [ + build_event( + EventName.DEVICE_STATE_CHANGED.value, + device_url=PERGOLA.device_url, + device_states=[ + { + "name": OverkizState.CORE_SLATE_ORIENTATION.value, + "type": 1, + "value": None, + }, + ], + ) + ], + ) + + state = hass.states.get(PERGOLA.entity_id) + assert ATTR_CURRENT_TILT_POSITION not in state.attributes + + +async def test_low_speed_cover_open_close( + hass: HomeAssistant, + setup_overkiz_integration: SetupOverkizIntegration, + mock_client: MockOverkizClient, +) -> None: + """Test low speed cover open and close send correct commands.""" + await setup_overkiz_integration(fixture=LOW_SPEED.fixture) + entity_id = "cover.nursery_shutter_low_speed" + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + assert_command_call( + mock_client, + device_url=LOW_SPEED.device_url, + command_name="setClosureAndLinearSpeed", + parameters=[0, OverkizCommandParam.LOWSPEED], + ) + + mock_client.execute_command.reset_mock() + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + assert_command_call( + mock_client, + device_url=LOW_SPEED.device_url, + command_name="setClosureAndLinearSpeed", + parameters=[100, OverkizCommandParam.LOWSPEED], + )