core/homeassistant/components/homee/cover.py

278 lines
10 KiB
Python
Raw Normal View History

"""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 (
attribute := self._node.get_attribute_by_type(AttributeType.POSITION)
) is not None:
homee_min = attribute.minimum
homee_max = attribute.maximum
homee_position = attribute.current_value
position = ((homee_position - homee_min) / (homee_max - homee_min)) * 100
return int(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 (
attribute := self._node.get_attribute_by_type(
AttributeType.SHUTTER_SLAT_POSITION
)
) is not None:
homee_min = attribute.minimum
homee_max = attribute.maximum
homee_position = attribute.current_value
position = ((homee_position - homee_min) / (homee_max - homee_min)) * 100
return int(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 (
attribute := self._node.get_attribute_by_type(AttributeType.POSITION)
) is not None:
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 (
attribute := self._node.get_attribute_by_type(
AttributeType.SHUTTER_SLAT_POSITION
)
) is not None:
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.
if (
attribute := self._node.get_attribute_by_type(AttributeType.POSITION)
) is not None:
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."""
if (
slat_attribute := self._node.get_attribute_by_type(
AttributeType.SLAT_ROTATION_IMPULSE
)
) is not None:
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."""
if (
slat_attribute := self._node.get_attribute_by_type(
AttributeType.SLAT_ROTATION_IMPULSE
)
) is not None:
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.
if (
attribute := self._node.get_attribute_by_type(
AttributeType.SHUTTER_SLAT_POSITION
)
) is not None:
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)