Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor zwave_js.cover #93433

Merged
merged 9 commits into from
May 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions homeassistant/components/zwave_js/const.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
"""Constants for the Z-Wave JS integration."""
from __future__ import annotations

import logging

from homeassistant.const import APPLICATION_NAME, __version__ as HA_VERSION
Expand Down
271 changes: 176 additions & 95 deletions homeassistant/components/zwave_js/cover.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
elif info.platform_hint and info.platform_hint.endswith("tilt"):
entities.append(ZWaveTiltCover(config_entry, driver, info))
else:
entities.append(ZWaveCover(config_entry, driver, info))
entities.append(ZWaveMultilevelSwitchCover(config_entry, driver, info))
async_add_entities(entities)

config_entry.async_on_unload(
Expand All @@ -66,161 +66,242 @@
)


def percent_to_zwave_position(value: int) -> int:
"""Convert position in 0-100 scale to 0-99 scale.
class CoverPositionMixin(ZWaveBaseEntity, CoverEntity):
"""Mix-in class for cover with position support."""

`value` -- (int) Position byte value from 0-100.
"""
if value > 0:
return max(1, round((value / 100) * 99))
return 0
_current_position_value: ZwaveValue | None = None
_target_position_value: ZwaveValue | None = None
_stop_position_value: ZwaveValue | None = None


def percent_to_zwave_tilt(value: int) -> int:
"""Convert position in 0-100 scale to 0-99 scale.

`value` -- (int) Position byte value from 0-100.
"""
if value > 0:
return round((value / 100) * 99)
return 0


def zwave_tilt_to_percent(value: int) -> int:
"""Convert 0-99 scale to position in 0-100 scale.

`value` -- (int) Position byte value from 0-99.
"""
if value > 0:
return round((value / 99) * 100)
return 0


class ZWaveCover(ZWaveBaseEntity, CoverEntity):
"""Representation of a Z-Wave Cover device."""

_attr_supported_features = (
CoverEntityFeature.OPEN
| CoverEntityFeature.CLOSE
| CoverEntityFeature.SET_POSITION
)

def __init__(
def _set_position_values(
self,
config_entry: ConfigEntry,
driver: Driver,
info: ZwaveDiscoveryInfo,
current_value: ZwaveValue,
target_value: ZwaveValue | None = None,
stop_value: ZwaveValue | None = None,
) -> None:
"""Initialize a ZWaveCover entity."""
super().__init__(config_entry, driver, info)

self._stop_cover_value = (
self.get_zwave_value(COVER_OPEN_PROPERTY)
or self.get_zwave_value(COVER_UP_PROPERTY)
or self.get_zwave_value(COVER_ON_PROPERTY)
"""Set values for position."""
self._attr_supported_features = (
(self._attr_supported_features or 0)
| CoverEntityFeature.OPEN
| CoverEntityFeature.CLOSE
| CoverEntityFeature.SET_POSITION
)
self._current_position_value = current_value
self._target_position_value = target_value or self.get_zwave_value(
TARGET_VALUE_PROPERTY, value_property_key=current_value.property_key
)

if self._stop_cover_value:
if stop_value:
self._stop_position_value = stop_value
self._attr_supported_features |= CoverEntityFeature.STOP

# Entity class attributes
self._attr_device_class = CoverDeviceClass.WINDOW
if self.info.platform_hint and self.info.platform_hint.startswith("shutter"):
self._attr_device_class = CoverDeviceClass.SHUTTER
if self.info.platform_hint and self.info.platform_hint.startswith("blind"):
self._attr_device_class = CoverDeviceClass.BLIND
def percent_to_zwave_position(self, value: int) -> int:
"""Convert position in 0-100 scale to closed_value-open_value scale."""
return (
jbouwh marked this conversation as resolved.
Show resolved Hide resolved
round(max(min(1, (value / 100)), 0) * self._position_range)
+ self._fully_closed_position
)

def zwave_to_percent_position(self, value: int) -> int:
"""Convert closed_value-open_value scale to position in 0-100 scale."""
return round(
((value - self._fully_closed_position) / self._position_range) * 100
)
jbouwh marked this conversation as resolved.
Show resolved Hide resolved

@property
def _fully_open_position(self) -> int:
"""Return value that represents fully opened position."""
max_ = self.info.primary_value.metadata.max
return 99 if max_ is None else max_

@property
def _fully_closed_position(self) -> int:
"""Return value that represents fully closed position."""
min_ = self.info.primary_value.metadata.min
return 0 if min_ is None else min_

@property
def _position_range(self) -> int:
"""Return range between fully opened and fully closed position."""
return self._fully_open_position - self._fully_closed_position

@property
def is_closed(self) -> bool | None:
"""Return true if cover is closed."""
if self.info.primary_value.value is None:
# guard missing value
if not (value := self._current_position_value) or value.value is None:
return None
return bool(self.info.primary_value.value == 0)
return bool(value.value == 0)

@property
def current_cover_position(self) -> int | None:
"""Return the current position of cover where 0 means closed and 100 is fully open."""
if self.info.primary_value.value is None:
if (
self._current_position_value is None
or self._current_position_value.value is None
):
# guard missing value
return None
return round((cast(int, self.info.primary_value.value) / 99) * 100)
return self.zwave_to_percent_position(self._current_position_value.value)

async def async_set_cover_position(self, **kwargs: Any) -> None:
"""Move the cover to a specific position."""
target_value = self.get_zwave_value(TARGET_VALUE_PROPERTY)
assert target_value is not None
assert self._target_position_value
await self.info.node.async_set_value(
target_value, percent_to_zwave_position(kwargs[ATTR_POSITION])
self._target_position_value,
self.percent_to_zwave_position(kwargs[ATTR_POSITION]),
)

async def async_open_cover(self, **kwargs: Any) -> None:
"""Open the cover."""
target_value = self.get_zwave_value(TARGET_VALUE_PROPERTY)
assert target_value is not None
await self.info.node.async_set_value(target_value, 99)
assert self._target_position_value
await self.info.node.async_set_value(
self._target_position_value, self._fully_open_position
)

async def async_close_cover(self, **kwargs: Any) -> None:
"""Close cover."""
target_value = self.get_zwave_value(TARGET_VALUE_PROPERTY)
assert target_value is not None
await self.info.node.async_set_value(target_value, 0)
assert self._target_position_value
await self.info.node.async_set_value(
self._target_position_value, self._fully_closed_position
)

async def async_stop_cover(self, **kwargs: Any) -> None:
"""Stop cover."""
assert self._stop_cover_value
assert self._stop_position_value
# Stop the cover, will stop regardless of the actual direction of travel.
await self.info.node.async_set_value(self._stop_cover_value, False)
await self.info.node.async_set_value(self._stop_position_value, False)


class ZWaveTiltCover(ZWaveCover):
"""Representation of a Z-Wave cover device with tilt."""
class CoverTiltMixin(ZWaveBaseEntity, CoverEntity):
"""Mix-in class for cover with tilt support."""

def __init__(
_current_tilt_value: ZwaveValue | None = None
_target_tilt_value: ZwaveValue | None = None
_stop_tilt_value: ZwaveValue | None = None

def _set_tilt_values(
self,
config_entry: ConfigEntry,
driver: Driver,
info: ZwaveDiscoveryInfo,
current_value: ZwaveValue,
target_value: ZwaveValue | None = None,
) -> None:
"""Initialize a ZWaveCover entity."""
super().__init__(config_entry, driver, info)

self._current_tilt_value = cast(
CoverTiltDataTemplate, self.info.platform_data_template
).current_tilt_value(self.info.platform_data)

self._attr_supported_features |= (
CoverEntityFeature.OPEN_TILT
"""Set values for tilt."""
self._attr_supported_features = (
(self._attr_supported_features or 0)
| CoverEntityFeature.OPEN_TILT
| CoverEntityFeature.CLOSE_TILT
| CoverEntityFeature.SET_TILT_POSITION
)
self._current_tilt_value = current_value
self._target_tilt_value = target_value or self.get_zwave_value(
TARGET_VALUE_PROPERTY, value_property_key=current_value.property_key
)

def percent_to_zwave_tilt(self, value: int) -> int:
"""Convert position in 0-100 scale to closed_value-open_value scale."""
return (

Check warning on line 201 in homeassistant/components/zwave_js/cover.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/zwave_js/cover.py#L201

Added line #L201 was not covered by tests
round(max(min(1, (value / 100)), 0) * self._tilt_range)
+ self._fully_closed_tilt
)

def zwave_to_percent_tilt(self, value: int) -> int:
"""Convert closed_value-open_value scale to position in 0-100 scale."""
return round(((value - self._fully_closed_tilt) / self._tilt_range) * 100)
jbouwh marked this conversation as resolved.
Show resolved Hide resolved

@property
def _fully_open_tilt(self) -> int:
"""Return value that represents fully opened tilt."""
max_ = self.info.primary_value.metadata.max
return 99 if max_ is None else max_

@property
def _fully_closed_tilt(self) -> int:
"""Return value that represents fully closed tilt."""
min_ = self.info.primary_value.metadata.min
return 0 if min_ is None else min_

@property
def _tilt_range(self) -> int:
"""Return range between fully opened and fully closed tilt."""
return self._fully_open_tilt - self._fully_closed_tilt

@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.
"""
value = self._current_tilt_value
if value is None or value.value is None:
if (value := self._current_tilt_value) is None or value.value is None:
return None
return zwave_tilt_to_percent(int(value.value))
return self.zwave_to_percent_tilt(int(value.value))

async def async_set_cover_tilt_position(self, **kwargs: Any) -> None:
"""Move the cover tilt to a specific position."""
assert self._current_tilt_value
assert self._target_tilt_value

Check warning on line 239 in homeassistant/components/zwave_js/cover.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/zwave_js/cover.py#L239

Added line #L239 was not covered by tests
await self.info.node.async_set_value(
self._current_tilt_value,
percent_to_zwave_tilt(kwargs[ATTR_TILT_POSITION]),
self._target_tilt_value,
self.percent_to_zwave_tilt(kwargs[ATTR_TILT_POSITION]),
)

async def async_open_cover_tilt(self, **kwargs: Any) -> None:
"""Open the cover tilt."""
await self.async_set_cover_tilt_position(tilt_position=100)
assert self._target_tilt_value
await self.info.node.async_set_value(
self._target_tilt_value, self._fully_open_tilt
)

async def async_close_cover_tilt(self, **kwargs: Any) -> None:
"""Close the cover tilt."""
await self.async_set_cover_tilt_position(tilt_position=0)
assert self._target_tilt_value
await self.info.node.async_set_value(
self._target_tilt_value, self._fully_closed_tilt
)


class ZWaveMultilevelSwitchCover(CoverPositionMixin):
"""Representation of a Z-Wave Cover that uses Multilevel Switch CC for position."""

def __init__(
self,
config_entry: ConfigEntry,
driver: Driver,
info: ZwaveDiscoveryInfo,
) -> None:
"""Initialize a ZWaveCover entity."""
super().__init__(config_entry, driver, info)
self._set_position_values(
self.info.primary_value,
stop_value=(
self.get_zwave_value(COVER_OPEN_PROPERTY)
or self.get_zwave_value(COVER_UP_PROPERTY)
or self.get_zwave_value(COVER_ON_PROPERTY)
),
)

# Entity class attributes
self._attr_device_class = CoverDeviceClass.WINDOW
if self.info.platform_hint and self.info.platform_hint.startswith("shutter"):
self._attr_device_class = CoverDeviceClass.SHUTTER
if self.info.platform_hint and self.info.platform_hint.startswith("blind"):
self._attr_device_class = CoverDeviceClass.BLIND


class ZWaveTiltCover(ZWaveMultilevelSwitchCover, CoverTiltMixin):
"""Representation of a Z-Wave cover device with tilt."""

def __init__(
self,
config_entry: ConfigEntry,
driver: Driver,
info: ZwaveDiscoveryInfo,
) -> None:
"""Initialize a ZWaveCover entity."""
super().__init__(config_entry, driver, info)

template = cast(CoverTiltDataTemplate, self.info.platform_data_template)
self._set_tilt_values(
template.current_tilt_value(self.info.platform_data),
template.target_tilt_value(self.info.platform_data),
)


class ZwaveMotorizedBarrier(ZWaveBaseEntity, CoverEntity):
Expand Down
12 changes: 9 additions & 3 deletions homeassistant/components/zwave_js/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -363,12 +363,18 @@ def get_config_parameter_discovery_schema(
product_type={0x0301, 0x0302},
primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA,
data_template=CoverTiltDataTemplate(
tilt_value_id=ZwaveValueID(
current_tilt_value_id=ZwaveValueID(
property_="fibaro",
command_class=CommandClass.MANUFACTURER_PROPRIETARY,
endpoint=0,
property_key="venetianBlindsTilt",
)
),
target_tilt_value_id=ZwaveValueID(
property_="fibaro",
command_class=CommandClass.MANUFACTURER_PROPRIETARY,
endpoint=0,
property_key="venetianBlindsTilt",
),
),
required_values=[
ZWaveValueDiscoverySchema(
Expand Down Expand Up @@ -854,7 +860,7 @@ def get_config_parameter_discovery_schema(
# window coverings
ZWaveDiscoverySchema(
platform=Platform.COVER,
hint="cover",
hint="multilevel_switch",
device_class_generic={"Multilevel Switch"},
device_class_specific={
"Motor Control Class A",
Expand Down