core/homeassistant/components/homee/cover.py

266 lines
9.8 KiB
Python

"""The homee cover platform."""
import logging
from typing import Any, cast
from pyHomee.const import AttributeType, NodeProfile
from pyHomee.model import HomeeAttribute, HomeeNode
from homeassistant.components.cover import (
ATTR_POSITION,
ATTR_TILT_POSITION,
CoverDeviceClass,
CoverEntity,
CoverEntityFeature,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import HomeeConfigEntry
from .entity import HomeeNodeEntity
_LOGGER = logging.getLogger(__name__)
OPEN_CLOSE_ATTRIBUTES = [
AttributeType.OPEN_CLOSE,
AttributeType.SLAT_ROTATION_IMPULSE,
AttributeType.UP_DOWN,
]
POSITION_ATTRIBUTES = [AttributeType.POSITION, AttributeType.SHUTTER_SLAT_POSITION]
def get_open_close_attribute(node: HomeeNode) -> HomeeAttribute | None:
"""Return the attribute used for opening/closing the cover."""
# We assume, that no device has UP_DOWN and OPEN_CLOSE, but only one of them.
if (open_close := node.get_attribute_by_type(AttributeType.UP_DOWN)) is None:
open_close = node.get_attribute_by_type(AttributeType.OPEN_CLOSE)
return open_close
def get_cover_features(
node: HomeeNode, open_close_attribute: HomeeAttribute | None
) -> CoverEntityFeature:
"""Determine the supported cover features of a homee node based on the available attributes."""
features = CoverEntityFeature(0)
if (open_close_attribute is not None) and open_close_attribute.editable:
features |= (
CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE | CoverEntityFeature.STOP
)
# Check for up/down position settable.
attribute = node.get_attribute_by_type(AttributeType.POSITION)
if attribute is not None:
if attribute.editable:
features |= CoverEntityFeature.SET_POSITION
if node.get_attribute_by_type(AttributeType.SLAT_ROTATION_IMPULSE) is not None:
features |= CoverEntityFeature.OPEN_TILT | CoverEntityFeature.CLOSE_TILT
if node.get_attribute_by_type(AttributeType.SHUTTER_SLAT_POSITION) is not None:
features |= CoverEntityFeature.SET_TILT_POSITION
return features
def get_device_class(node: HomeeNode) -> CoverDeviceClass | None:
"""Determine the device class a homee node based on the node profile."""
COVER_DEVICE_PROFILES = {
NodeProfile.GARAGE_DOOR_OPERATOR: CoverDeviceClass.GARAGE,
NodeProfile.SHUTTER_POSITION_SWITCH: CoverDeviceClass.SHUTTER,
}
return COVER_DEVICE_PROFILES.get(node.profile)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HomeeConfigEntry,
async_add_devices: AddEntitiesCallback,
) -> None:
"""Add the homee platform for the cover integration."""
async_add_devices(
HomeeCover(node, config_entry)
for node in config_entry.runtime_data.nodes
if is_cover_node(node)
)
def is_cover_node(node: HomeeNode) -> bool:
"""Determine if a node is controllable as a homee cover based on its profile and attributes."""
return node.profile in [
NodeProfile.ELECTRIC_MOTOR_METERING_SWITCH,
NodeProfile.ELECTRIC_MOTOR_METERING_SWITCH_WITHOUT_SLAT_POSITION,
NodeProfile.GARAGE_DOOR_OPERATOR,
NodeProfile.SHUTTER_POSITION_SWITCH,
]
class HomeeCover(HomeeNodeEntity, CoverEntity):
"""Representation of a homee cover device."""
_attr_name = None
def __init__(self, node: HomeeNode, entry: HomeeConfigEntry) -> None:
"""Initialize a homee cover entity."""
super().__init__(node, entry)
self._open_close_attribute = get_open_close_attribute(node)
self._attr_supported_features = get_cover_features(
node, self._open_close_attribute
)
self._attr_device_class = get_device_class(node)
self._attr_unique_id = (
f"{self._attr_unique_id}-{self._open_close_attribute.id}"
if self._open_close_attribute is not None
else f"{self._attr_unique_id}-0"
)
@property
def current_cover_position(self) -> int | None:
"""Return the cover's position."""
# Translate the homee position values to HA's 0-100 scale
if self.has_attribute(AttributeType.POSITION):
attribute = self._node.get_attribute_by_type(AttributeType.POSITION)
homee_min = attribute.minimum
homee_max = attribute.maximum
homee_position = attribute.current_value
position = ((homee_position - homee_min) / (homee_max - homee_min)) * 100
return 100 - position
return None
@property
def current_cover_tilt_position(self) -> int | None:
"""Return the cover's tilt position."""
# Translate the homee position values to HA's 0-100 scale
if self.has_attribute(AttributeType.SHUTTER_SLAT_POSITION):
attribute = self._node.get_attribute_by_type(
AttributeType.SHUTTER_SLAT_POSITION
)
homee_min = attribute.minimum
homee_max = attribute.maximum
homee_position = attribute.current_value
position = ((homee_position - homee_min) / (homee_max - homee_min)) * 100
return 100 - position
return None
@property
def is_opening(self) -> bool | None:
"""Return the opening status of the cover."""
if self._open_close_attribute is not None:
return (
self._open_close_attribute.get_value() == 3
if not self._open_close_attribute.is_reversed
else self._open_close_attribute.get_value() == 4
)
return None
@property
def is_closing(self) -> bool | None:
"""Return the closing status of the cover."""
if self._open_close_attribute is not None:
return (
self._open_close_attribute.get_value() == 4
if not self._open_close_attribute.is_reversed
else self._open_close_attribute.get_value() == 3
)
return None
@property
def is_closed(self) -> bool | None:
"""Return if the cover is closed."""
if self.has_attribute(AttributeType.POSITION):
attribute = self._node.get_attribute_by_type(AttributeType.POSITION)
return attribute.get_value() == attribute.maximum
if self._open_close_attribute is not None:
if not self._open_close_attribute.is_reversed:
return self._open_close_attribute.get_value() == 1
return self._open_close_attribute.get_value() == 0
# If none of the above is present, it might be a slat only cover.
if self.has_attribute(AttributeType.SHUTTER_SLAT_POSITION):
attribute = self._node.get_attribute_by_type(
AttributeType.SHUTTER_SLAT_POSITION
)
return attribute.get_value() == attribute.minimum
return None
async def async_open_cover(self, **kwargs: Any) -> None:
"""Open the cover."""
assert self._open_close_attribute is not None
if not self._open_close_attribute.is_reversed:
await self.async_set_value(self._open_close_attribute, 0)
else:
await self.async_set_value(self._open_close_attribute, 1)
async def async_close_cover(self, **kwargs: Any) -> None:
"""Close cover."""
assert self._open_close_attribute is not None
if not self._open_close_attribute.is_reversed:
await self.async_set_value(self._open_close_attribute, 1)
else:
await self.async_set_value(self._open_close_attribute, 0)
async def async_set_cover_position(self, **kwargs: Any) -> None:
"""Move the cover to a specific position."""
if CoverEntityFeature.SET_POSITION in self.supported_features:
position = 100 - cast(int, kwargs[ATTR_POSITION])
# Convert position to range of our entity.
attribute = self._node.get_attribute_by_type(AttributeType.POSITION)
homee_min = attribute.minimum
homee_max = attribute.maximum
homee_position = (position / 100) * (homee_max - homee_min) + homee_min
await self.async_set_value(attribute, homee_position)
async def async_stop_cover(self, **kwargs: Any) -> None:
"""Stop the cover."""
if self._open_close_attribute is not None:
await self.async_set_value(self._open_close_attribute, 2)
async def async_open_cover_tilt(self, **kwargs: Any) -> None:
"""Open the cover tilt."""
slat_attribute = self._node.get_attribute_by_type(
AttributeType.SLAT_ROTATION_IMPULSE
)
if not slat_attribute.is_reversed:
await self.async_set_value(slat_attribute, 2)
else:
await self.async_set_value(slat_attribute, 1)
async def async_close_cover_tilt(self, **kwargs: Any) -> None:
"""Close the cover tilt."""
slat_attribute = self._node.get_attribute_by_type(
AttributeType.SLAT_ROTATION_IMPULSE
)
if not slat_attribute.is_reversed:
await self.async_set_value(slat_attribute, 1)
else:
await self.async_set_value(slat_attribute, 2)
async def async_set_cover_tilt_position(self, **kwargs: Any) -> None:
"""Move the cover tilt to a specific position."""
if CoverEntityFeature.SET_TILT_POSITION in self.supported_features:
position = 100 - cast(int, kwargs[ATTR_TILT_POSITION])
# Convert position to range of our entity.
attribute = self._node.get_attribute_by_type(
AttributeType.SHUTTER_SLAT_POSITION
)
homee_min = attribute.minimum
homee_max = attribute.maximum
homee_position = (position / 100) * (homee_max - homee_min) + homee_min
await self.async_set_value(attribute, homee_position)