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

zwave_js: cover: Fibaro FGR-222 venetian blind tilt support #49371

Closed
wants to merge 1 commit into from
Closed
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
195 changes: 191 additions & 4 deletions homeassistant/components/zwave_js/cover.py
Expand Up @@ -5,14 +5,22 @@
from typing import Any, Callable

from zwave_js_server.client import Client as ZwaveClient
from zwave_js_server.model.value import Value as ZwaveValue
from zwave_js_server.const import CommandClass
from zwave_js_server.model.node import Node as ZwaveNode
from zwave_js_server.model.value import Value as ZwaveValue, get_value_id

from homeassistant.components.cover import (
ATTR_POSITION,
ATTR_TILT_POSITION,
DEVICE_CLASS_GARAGE,
DOMAIN as COVER_DOMAIN,
SUPPORT_CLOSE,
SUPPORT_CLOSE_TILT,
SUPPORT_OPEN,
SUPPORT_OPEN_TILT,
SUPPORT_SET_POSITION,
SUPPORT_SET_TILT_POSITION,
SUPPORT_STOP,
CoverEntity,
)
from homeassistant.config_entries import ConfigEntry
Expand All @@ -34,6 +42,61 @@
BARRIER_STATE_OPENING = 254
BARRIER_STATE_OPEN = 255

FIBARO_FGR222_CONFIGURATION_REPORTS_TYPE_PROPERTY = 3
FIBARO_FGR222_CONFIGURATION_REPORTS_TYPE_MANUFAC_PROP = 1
FIBARO_FGR222_CONFIGURATION_OPERATING_MODE_PROPERTY = 10
FIBARO_FGR222_CONFIGURATION_OPERATING_MODE_VENETIAN_WITH_POSITION = 2
FIBARO_FGR222_MANUFACTURER_PROPRIETARY_BLINDS_POSITION_PROPERTY = (
"fibaro-venetianBlindsPosition"
)
FIBARO_FGR222_MANUFACTURER_PROPRIETARY_BLINDS_TILT_PROPERTY = (
"fibaro-venetianBlindsTilt"
)


def get_node_configuration_value(node: ZwaveNode, property_: int) -> Any | None:
"""Return a node's configuration value, if any."""
config_values = node.get_configuration_values()

value_id = get_value_id(
node,
CommandClass.CONFIGURATION,
property_,
endpoint=0,
)
if value_id not in config_values:
return None

return config_values[value_id].value


def is_fgr222_in_venetian_config(info: ZwaveDiscoveryInfo) -> bool:
"""Check if node is an FGR222 in sane venetian blinds configuration."""
if info.platform_hint != "fibaro_fgr222":
return False

# Is the node reporting through the "Manufacturer Proprietary" value?
if (
get_node_configuration_value(
info.node, FIBARO_FGR222_CONFIGURATION_REPORTS_TYPE_PROPERTY
)
is not FIBARO_FGR222_CONFIGURATION_REPORTS_TYPE_MANUFAC_PROP
):
return False

# Is the node configured as "venetian blind mode with positioning"?
if (
get_node_configuration_value(
info.node, FIBARO_FGR222_CONFIGURATION_OPERATING_MODE_PROPERTY
)
is not FIBARO_FGR222_CONFIGURATION_OPERATING_MODE_VENETIAN_WITH_POSITION
):
return False

LOGGER.debug("Node %u detected as FGR222 in venetian config", info.node.node_id)

return True


async def async_setup_entry(
hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable
Expand All @@ -47,6 +110,8 @@ def async_add_cover(info: ZwaveDiscoveryInfo) -> None:
entities: list[ZWaveBaseEntity] = []
if info.platform_hint == "motorized_barrier":
entities.append(ZwaveMotorizedBarrier(config_entry, client, info))
elif is_fgr222_in_venetian_config(info):
entities.append(FGR222Venetian(config_entry, client, info))
else:
entities.append(ZWaveCover(config_entry, client, info))
async_add_entities(entities)
Expand All @@ -70,24 +135,35 @@ def percent_to_zwave_position(value: int) -> int:
return 0


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

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


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

@property
def is_closed(self) -> bool | None:
"""Return true if cover is closed."""
"""Return if the cover is closed or not."""
if self.info.primary_value.value is None:
# guard missing value
return None
return bool(self.info.primary_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."""
"""Return current position of cover.

None is unknown, 0 is closed, 100 is fully open.
"""
if self.info.primary_value.value is None:
# guard missing value
return None
return round((self.info.primary_value.value / 99) * 100)
return zwave_position_to_percent(self.info.primary_value.value)

async def async_set_cover_position(self, **kwargs: Any) -> None:
"""Move the cover to a specific position."""
Expand Down Expand Up @@ -116,6 +192,117 @@ async def async_stop_cover(self, **kwargs: Any) -> None:
await self.info.node.async_set_value(target_value, False)


class FGR222Venetian(ZWaveCover):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably don't want to add device specific code to the platforms. That should be solved some other way, preferably upstream in the zwave-js project.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

zwave_js implements the Z-Wave protocol. AFAICS (please correct me if I'm wrong), the Manufacturer Proprietary command class of the Z-Wave protocol is explicitly designed to contain device-specializations. By providing access to the Manufacturer Proprietary values, zwave_js is doing its part by implementing the transport layer. It mustn't know about semantics of the payload.

It's up to the driver to make use of the payload, and the driver is home-assistant. We had the same approach when we implemented this for OpenZWave (#29701) and it was accepted back then.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll only accept this PR if the device specific code can be abstracted into a problem case in discovery like we did here for dynamic climate temperature values: #49804

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understand the ask, but I won't have time to re-design this. So feel free to close this if no-one wants to step in.

Two thoughts before you do that:
First, in the spirit of pragmatism, why introduce an abstraction for a problem now, before it is clear if there ever will be a second user of said abstraction (there might be, not denying that). The PR in its current shape will introduce less code than with the abstraction. Why not defer it to the time a second user emerges? The PR as it is right now will probably enable a bunch of users of the FGR222 right away (https://community.home-assistant.io/t/fibaro-fgrm222-venetian-blind-tilt-position/50710/17)

Second, each device that has some manufacturer-proprietary characteristics might have some unique quirks here or there. For the FGR222, for example, it is the tilt-position interfering with the reported cover position (https://github.com/home-assistant/core/pull/49371/files#diff-e6292e73b3e851acd39ec60f47355d032083c995521448faa206c8105c620fe6R252-R255). Of course, you can abstract over this specific problem as well, but doing that for each new device can bloat the abstraction over time as well. I still think that viewing Z-Wave as a transport / communication means and not as the driver itself makes sense as well.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't want device specific code in the platforms. I don't think we'll accept the workaround for the tilt. That should be fixed by the device.

"""Implementation of the FGR-222 in proprietary venetian configuration.

This adds support for the tilt feature for the ventian blind mode.

To enable this, the following node configuration values must be set:
* Set "3: Reports type to Blind position reports sent"
to value "the main controller using Fibaro Command Class"
* Set "10: Roller Shutter operating modes"
to value "2 - Venetian Blind Mode, with positioning"
"""

def __init__(
self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo
) -> None:
"""Initialize the FGR-222."""
super().__init__(config_entry, client, info)

self._blinds_position = self.get_zwave_value(
FIBARO_FGR222_MANUFACTURER_PROPRIETARY_BLINDS_POSITION_PROPERTY,
command_class=CommandClass.MANUFACTURER_PROPRIETARY,
add_to_watched_value_ids=True,
)

self._tilt_position = self.get_zwave_value(
FIBARO_FGR222_MANUFACTURER_PROPRIETARY_BLINDS_TILT_PROPERTY,
command_class=CommandClass.MANUFACTURER_PROPRIETARY,
add_to_watched_value_ids=True,
)

@property
def is_closed(self) -> bool | None:
"""Return if the cover is closed or not."""
pos = self.current_cover_position
if pos is None:
return None

return bool(pos == 0)

@property
def current_cover_position(self) -> int | None:
"""Return current position of cover.

None is unknown, 0 is closed, 100 is fully open.
"""
if self._blinds_position is None:
return None
if self._blinds_position.value is None:
return None

# On the FGR-222, when it is controlling venetian blinds, it can happen that the cover
# position can't reach 0 or 99. On top of that, the tilt position can influence the reported
# cover position as well. That is, fully open or fully closed blinds can shift the blinds
# position value.
#
# Hence, saturate a bit earlier in each direction.
pos = self._blinds_position.value # This is a Zwave value, so range is: 0-99
if pos < 4:
pos = 0
if pos > 95:
pos = 99

return zwave_position_to_percent(pos)

@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 self._tilt_position is None:
return None
if self._tilt_position.value is None:
return None

return zwave_position_to_percent(self._tilt_position.value)

@property
def supported_features(self) -> int:
"""Flag supported features."""
supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP

if self.current_cover_position is not None:
supported_features |= SUPPORT_SET_POSITION

if self.current_cover_tilt_position is not None:
supported_features |= (
SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT | SUPPORT_SET_TILT_POSITION
)

return supported_features

async def async_open_cover_tilt(self, **kwargs: Any) -> None:
"""Open the cover tilt."""
if self._tilt_position:
await self.info.node.async_set_value(self._tilt_position, 99)

async def async_close_cover_tilt(self, **kwargs: Any) -> None:
"""Close the cover tilt."""
if self._tilt_position:
await self.info.node.async_set_value(self._tilt_position, 0)

async def async_set_cover_tilt_position(self, **kwargs: Any) -> None:
"""Move the cover tilt to a specific position."""
if self._tilt_position:
await self.info.node.async_set_value(
self._tilt_position,
percent_to_zwave_position(kwargs[ATTR_TILT_POSITION]),
)


class ZwaveMotorizedBarrier(ZWaveBaseEntity, CoverEntity):
"""Representation of a Z-Wave motorized barrier device."""

Expand Down
3 changes: 2 additions & 1 deletion homeassistant/components/zwave_js/discovery.py
Expand Up @@ -164,9 +164,10 @@ def get_config_parameter_discovery_schema(
type={"number"},
),
),
# Fibaro Shutter Fibaro FGS222
# Fibaro Shutter Fibaro FGR-222
ZWaveDiscoverySchema(
platform="cover",
hint="fibaro_fgr222",
manufacturer_id={0x010F},
product_id={0x1000},
product_type={0x0302},
Expand Down