mirror of https://github.com/home-assistant/core
266 lines
9.8 KiB
Python
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)
|