Add cover platform to Overkiz integration (#64564)

This commit is contained in:
Mick Vleeshouwer 2022-01-24 09:58:52 -08:00 committed by GitHub
parent 78e92d1662
commit d6c547e9a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 431 additions and 3 deletions

View File

@ -820,6 +820,8 @@ omit =
homeassistant/components/overkiz/__init__.py homeassistant/components/overkiz/__init__.py
homeassistant/components/overkiz/binary_sensor.py homeassistant/components/overkiz/binary_sensor.py
homeassistant/components/overkiz/button.py homeassistant/components/overkiz/button.py
homeassistant/components/overkiz/cover.py
homeassistant/components/overkiz/cover_entities/*
homeassistant/components/overkiz/coordinator.py homeassistant/components/overkiz/coordinator.py
homeassistant/components/overkiz/diagnostics.py homeassistant/components/overkiz/diagnostics.py
homeassistant/components/overkiz/entity.py homeassistant/components/overkiz/entity.py

View File

@ -22,6 +22,7 @@ UPDATE_INTERVAL_ALL_ASSUMED_STATE: Final = timedelta(minutes=60)
PLATFORMS: list[Platform] = [ PLATFORMS: list[Platform] = [
Platform.BINARY_SENSOR, Platform.BINARY_SENSOR,
Platform.BUTTON, Platform.BUTTON,
Platform.COVER,
Platform.LIGHT, Platform.LIGHT,
Platform.LOCK, Platform.LOCK,
Platform.NUMBER, Platform.NUMBER,
@ -37,13 +38,29 @@ IGNORED_OVERKIZ_DEVICES: list[UIClass | UIWidget] = [
# Used to map the Somfy widget and ui_class to the Home Assistant platform # Used to map the Somfy widget and ui_class to the Home Assistant platform
OVERKIZ_DEVICE_TO_PLATFORM: dict[UIClass | UIWidget, Platform] = { OVERKIZ_DEVICE_TO_PLATFORM: dict[UIClass | UIWidget, Platform] = {
UIClass.ADJUSTABLE_SLATS_ROLLER_SHUTTER: Platform.COVER,
UIClass.AWNING: Platform.COVER,
UIClass.CURTAIN: Platform.COVER,
UIClass.DOOR_LOCK: Platform.LOCK, UIClass.DOOR_LOCK: Platform.LOCK,
UIWidget.DOMESTIC_HOT_WATER_TANK: Platform.SWITCH, # widgetName, uiClass is WaterHeatingSystem (not supported) UIClass.EXTERIOR_SCREEN: Platform.COVER,
UIClass.EXTERIOR_VENETIAN_BLIND: Platform.COVER,
UIClass.GARAGE_DOOR: Platform.COVER,
UIClass.GATE: Platform.COVER,
UIClass.LIGHT: Platform.LIGHT, UIClass.LIGHT: Platform.LIGHT,
UIClass.ON_OFF: Platform.SWITCH, UIClass.ON_OFF: Platform.SWITCH,
UIClass.PERGOLA: Platform.COVER,
UIClass.ROLLER_SHUTTER: Platform.COVER,
UIClass.SCREEN: Platform.COVER,
UIClass.SHUTTER: Platform.COVER,
UIClass.SWIMMING_POOL: Platform.SWITCH,
UIClass.SWINGING_SHUTTER: Platform.COVER,
UIClass.VENETIAN_BLIND: Platform.COVER,
UIClass.WINDOW: Platform.COVER,
UIWidget.DOMESTIC_HOT_WATER_TANK: Platform.SWITCH, # widgetName, uiClass is WaterHeatingSystem (not supported)
UIWidget.MY_FOX_SECURITY_CAMERA: Platform.COVER, # widgetName, uiClass is Camera (not supported)
UIWidget.RTD_INDOOR_SIREN: Platform.SWITCH, # widgetName, uiClass is Siren (not supported) UIWidget.RTD_INDOOR_SIREN: Platform.SWITCH, # widgetName, uiClass is Siren (not supported)
UIWidget.RTD_OUTDOOR_SIREN: Platform.SWITCH, # widgetName, uiClass is Siren (not supported) UIWidget.RTD_OUTDOOR_SIREN: Platform.SWITCH, # widgetName, uiClass is Siren (not supported)
UIClass.SWIMMING_POOL: Platform.SWITCH, UIWidget.RTS_GENERIC: Platform.COVER, # widgetName, uiClass is Generic (not supported)
} }
# Map Overkiz camelCase to Home Assistant snake_case for translation # Map Overkiz camelCase to Home Assistant snake_case for translation

View File

@ -0,0 +1,34 @@
"""Support for Overkiz covers - shutters etc."""
from pyoverkiz.enums import UIClass
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import HomeAssistantOverkizData
from .const import DOMAIN
from .cover_entities.awning import Awning
from .cover_entities.generic_cover import OverkizGenericCover
from .cover_entities.vertical_cover import VerticalCover
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the Overkiz covers from a config entry."""
data: HomeAssistantOverkizData = hass.data[DOMAIN][entry.entry_id]
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
]
async_add_entities(entities)

View File

@ -0,0 +1 @@
"""Cover entities for the Overkiz (by Somfy) integration."""

View File

@ -0,0 +1,69 @@
"""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,
DEVICE_CLASS_AWNING,
SUPPORT_CLOSE,
SUPPORT_OPEN,
SUPPORT_SET_POSITION,
SUPPORT_STOP,
)
from .generic_cover import COMMANDS_STOP, OverkizGenericCover
class Awning(OverkizGenericCover):
"""Representation of an Overkiz awning."""
_attr_device_class = DEVICE_CLASS_AWNING
@property
def supported_features(self) -> int:
"""Flag supported features."""
supported_features: int = super().supported_features
if self.executor.has_command(OverkizCommand.SET_DEPLOYMENT):
supported_features |= SUPPORT_SET_POSITION
if self.executor.has_command(OverkizCommand.DEPLOY):
supported_features |= SUPPORT_OPEN
if self.executor.has_command(*COMMANDS_STOP):
supported_features |= SUPPORT_STOP
if self.executor.has_command(OverkizCommand.UNDEPLOY):
supported_features |= SUPPORT_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.
"""
if current_position := self.executor.select_state(OverkizState.CORE_DEPLOYMENT):
return cast(int, current_position)
return None
async def async_set_cover_position(self, **kwargs: Any) -> None:
"""Move the cover to a specific position."""
position = kwargs.get(ATTR_POSITION, 0)
await self.executor.async_execute_command(
OverkizCommand.SET_DEPLOYMENT, 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)

View File

@ -0,0 +1,191 @@
"""Base class for Overkiz covers, shutters, awnings, etc."""
from __future__ import annotations
from collections.abc import Mapping
from typing import Any, cast
from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState
from homeassistant.components.cover import (
ATTR_TILT_POSITION,
SUPPORT_CLOSE_TILT,
SUPPORT_OPEN_TILT,
SUPPORT_SET_TILT_POSITION,
SUPPORT_STOP_TILT,
CoverEntity,
)
from homeassistant.components.overkiz.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,
OverkizCommand.CYCLE,
]
COMMANDS_OPEN_TILT: list[OverkizCommand] = [OverkizCommand.OPEN_SLATS]
COMMANDS_CLOSE: list[OverkizCommand] = [
OverkizCommand.CLOSE,
OverkizCommand.DOWN,
OverkizCommand.CYCLE,
]
COMMANDS_CLOSE_TILT: list[OverkizCommand] = [OverkizCommand.CLOSE_SLATS]
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.
"""
if position := self.executor.select_state(
OverkizState.CORE_SLATS_ORIENTATION, OverkizState.CORE_SLATE_ORIENTATION
):
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.get(ATTR_TILT_POSITION, 0),
)
@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)
@property
def is_opening(self) -> bool | None:
"""Return if the cover is opening or not."""
if self.assumed_state:
return None
# Check if cover movement execution is currently running
if any(
execution.get("device_url") == self.device.device_url
and execution.get("command_name") in COMMANDS_OPEN + COMMANDS_OPEN_TILT
for execution in self.coordinator.executions.values()
):
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.assumed_state:
return None
# Check if cover movement execution is currently running
if any(
execution.get("device_url") == self.device.device_url
and execution.get("command_name") in COMMANDS_CLOSE + COMMANDS_CLOSE_TILT
for execution in self.coordinator.executions.values()
):
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 extra_state_attributes(self) -> Mapping[str, Any] | None:
"""Return the device state attributes."""
attr = super().extra_state_attributes or {}
# Obstruction Detected attribute is used by HomeKit
if self.executor.has_state(OverkizState.IO_PRIORITY_LOCK_LEVEL):
return {**attr, **{ATTR_OBSTRUCTION_DETECTED: True}}
return attr
@property
def supported_features(self) -> int:
"""Flag supported features."""
supported_features = 0
if self.executor.has_command(*COMMANDS_OPEN_TILT):
supported_features |= SUPPORT_OPEN_TILT
if self.executor.has_command(*COMMANDS_STOP_TILT):
supported_features |= SUPPORT_STOP_TILT
if self.executor.has_command(*COMMANDS_CLOSE_TILT):
supported_features |= SUPPORT_CLOSE_TILT
if self.executor.has_command(*COMMANDS_SET_TILT_POSITION):
supported_features |= SUPPORT_SET_TILT_POSITION
return supported_features

View File

@ -0,0 +1,111 @@
"""Support for Overkiz Vertical Covers."""
from __future__ import annotations
from typing import Any, Union, cast
from pyoverkiz.enums import OverkizCommand, OverkizState, UIClass, UIWidget
from homeassistant.components.cover import (
ATTR_POSITION,
DEVICE_CLASS_AWNING,
DEVICE_CLASS_BLIND,
DEVICE_CLASS_CURTAIN,
DEVICE_CLASS_GARAGE,
DEVICE_CLASS_GATE,
DEVICE_CLASS_SHUTTER,
DEVICE_CLASS_WINDOW,
SUPPORT_CLOSE,
SUPPORT_OPEN,
SUPPORT_SET_POSITION,
SUPPORT_STOP,
)
from .generic_cover import 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: DEVICE_CLASS_CURTAIN,
UIClass.EXTERIOR_SCREEN: DEVICE_CLASS_BLIND,
UIClass.EXTERIOR_VENETIAN_BLIND: DEVICE_CLASS_BLIND,
UIClass.GARAGE_DOOR: DEVICE_CLASS_GARAGE,
UIClass.GATE: DEVICE_CLASS_GATE,
UIWidget.MY_FOX_SECURITY_CAMERA: DEVICE_CLASS_SHUTTER,
UIClass.PERGOLA: DEVICE_CLASS_AWNING,
UIClass.ROLLER_SHUTTER: DEVICE_CLASS_SHUTTER,
UIClass.SWINGING_SHUTTER: DEVICE_CLASS_SHUTTER,
UIClass.WINDOW: DEVICE_CLASS_WINDOW,
}
class VerticalCover(OverkizGenericCover):
"""Representation of an Overkiz vertical cover."""
@property
def supported_features(self) -> int:
"""Flag supported features."""
supported_features: int = super().supported_features
if self.executor.has_command(OverkizCommand.SET_CLOSURE):
supported_features |= SUPPORT_SET_POSITION
if self.executor.has_command(*COMMANDS_OPEN):
supported_features |= SUPPORT_OPEN
if self.executor.has_command(*COMMANDS_STOP):
supported_features |= SUPPORT_STOP
if self.executor.has_command(*COMMANDS_CLOSE):
supported_features |= SUPPORT_CLOSE
return supported_features
@property
def device_class(self) -> str:
"""Return the class of the device."""
return cast(
str,
(
OVERKIZ_DEVICE_TO_DEVICE_CLASS.get(self.device.widget)
or OVERKIZ_DEVICE_TO_DEVICE_CLASS.get(self.device.ui_class)
or DEVICE_CLASS_BLIND
),
)
@property
def current_cover_position(self) -> int | None:
"""
Return current position of cover.
None is unknown, 0 is closed, 100 is fully open.
"""
position = cast(
Union[int, None],
self.executor.select_state(
OverkizState.CORE_CLOSURE,
OverkizState.CORE_CLOSURE_OR_ROCKER_POSITION,
OverkizState.CORE_PEDESTRIAN_POSITION,
),
)
# Uno devices can have a position not in 0 to 100 range when unknown
if position is None or position < 0 or position > 100:
return None
return 100 - position
async def async_set_cover_position(self, **kwargs: Any) -> None:
"""Move the cover to a specific position."""
position = 100 - kwargs.get(ATTR_POSITION, 0)
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)

View File

@ -4,6 +4,7 @@ from __future__ import annotations
from typing import Any from typing import Any
from urllib.parse import urlparse from urllib.parse import urlparse
from pyoverkiz.enums.command import OverkizCommand
from pyoverkiz.models import Command, Device from pyoverkiz.models import Command, Device
from pyoverkiz.types import StateType as OverkizStateType from pyoverkiz.types import StateType as OverkizStateType
@ -76,7 +77,9 @@ class OverkizExecutor:
await self.coordinator.async_refresh() await self.coordinator.async_refresh()
async def async_cancel_command(self, commands_to_cancel: list[str]) -> bool: async def async_cancel_command(
self, commands_to_cancel: list[OverkizCommand]
) -> bool:
"""Cancel running execution by command.""" """Cancel running execution by command."""
# Cancel a running execution # Cancel a running execution