diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index 411d8b911b8..27d4dc00368 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -3,6 +3,10 @@ from __future__ import annotations import logging +from zwave_js_server.const.command_class.window_covering import ( + WindowCoveringPropertyKey, +) + from homeassistant.const import APPLICATION_NAME, __version__ as HA_VERSION USER_AGENT = {APPLICATION_NAME: HA_VERSION} @@ -131,3 +135,35 @@ ENTITY_DESC_KEY_TOTAL_INCREASING = "total_increasing" API_KEY_FIRMWARE_UPDATE_SERVICE = ( "2e39d98fc56386389fbb35e5a98fa1b44b9fdd8f971460303587cff408430d4cfcde6134" ) + +# Platform constants +# cover +COVER_POSITION_PROPERTY_KEYS: set[str | int | None] = { + WindowCoveringPropertyKey.INBOUND_BOTTOM, + WindowCoveringPropertyKey.INBOUND_BOTTOM_NO_POSITION, + WindowCoveringPropertyKey.INBOUND_LEFT, + WindowCoveringPropertyKey.INBOUND_LEFT_NO_POSITION, + WindowCoveringPropertyKey.INBOUND_LEFT_RIGHT, + WindowCoveringPropertyKey.INBOUND_LEFT_RIGHT_NO_POSITION, + WindowCoveringPropertyKey.INBOUND_RIGHT, + WindowCoveringPropertyKey.INBOUND_RIGHT_NO_POSITION, + WindowCoveringPropertyKey.INBOUND_TOP, + WindowCoveringPropertyKey.INBOUND_TOP_NO_POSITION, + WindowCoveringPropertyKey.INBOUND_TOP_BOTTOM, + WindowCoveringPropertyKey.INBOUND_TOP_BOTTOM_NO_POSITION, + WindowCoveringPropertyKey.OUTBOUND_BOTTOM, + WindowCoveringPropertyKey.OUTBOUND_BOTTOM_NO_POSITION, + WindowCoveringPropertyKey.OUTBOUND_LEFT, + WindowCoveringPropertyKey.OUTBOUND_LEFT_NO_POSITION, + WindowCoveringPropertyKey.OUTBOUND_RIGHT, + WindowCoveringPropertyKey.OUTBOUND_RIGHT_NO_POSITION, + WindowCoveringPropertyKey.OUTBOUND_TOP, + WindowCoveringPropertyKey.OUTBOUND_TOP_NO_POSITION, +} + +COVER_TILT_PROPERTY_KEYS: set[str | int | None] = { + WindowCoveringPropertyKey.HORIZONTAL_SLATS_ANGLE, + WindowCoveringPropertyKey.HORIZONTAL_SLATS_ANGLE_NO_POSITION, + WindowCoveringPropertyKey.VERTICAL_SLATS_ANGLE, + WindowCoveringPropertyKey.VERTICAL_SLATS_ANGLE_NO_POSITION, +} diff --git a/homeassistant/components/zwave_js/cover.py b/homeassistant/components/zwave_js/cover.py index 907d98f9c33..1521d6e567f 100644 --- a/homeassistant/components/zwave_js/cover.py +++ b/homeassistant/components/zwave_js/cover.py @@ -4,13 +4,23 @@ from __future__ import annotations from typing import Any, cast from zwave_js_server.client import Client as ZwaveClient -from zwave_js_server.const import TARGET_STATE_PROPERTY, TARGET_VALUE_PROPERTY +from zwave_js_server.const import ( + CURRENT_VALUE_PROPERTY, + TARGET_STATE_PROPERTY, + TARGET_VALUE_PROPERTY, +) from zwave_js_server.const.command_class.barrier_operator import BarrierState from zwave_js_server.const.command_class.multilevel_switch import ( COVER_ON_PROPERTY, COVER_OPEN_PROPERTY, COVER_UP_PROPERTY, ) +from zwave_js_server.const.command_class.window_covering import ( + NO_POSITION_PROPERTY_KEYS, + NO_POSITION_SUFFIX, + WINDOW_COVERING_OPEN_PROPERTY, + SlatStates, +) from zwave_js_server.model.driver import Driver from zwave_js_server.model.value import Value as ZwaveValue @@ -27,7 +37,12 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DATA_CLIENT, DOMAIN +from .const import ( + COVER_POSITION_PROPERTY_KEYS, + COVER_TILT_PROPERTY_KEYS, + DATA_CLIENT, + DOMAIN, +) from .discovery import ZwaveDiscoveryInfo from .discovery_data_template import CoverTiltDataTemplate from .entity import ZWaveBaseEntity @@ -49,7 +64,9 @@ async def async_setup_entry( driver = client.driver assert driver is not None # Driver is ready before platforms are loaded. entities: list[ZWaveBaseEntity] = [] - if info.platform_hint == "motorized_barrier": + if info.platform_hint == "window_covering": + entities.append(ZWaveWindowCovering(config_entry, driver, info)) + elif info.platform_hint == "motorized_barrier": entities.append(ZwaveMotorizedBarrier(config_entry, driver, info)) elif info.platform_hint and info.platform_hint.endswith("tilt"): entities.append(ZWaveTiltCover(config_entry, driver, info)) @@ -130,7 +147,7 @@ class CoverPositionMixin(ZWaveBaseEntity, CoverEntity): """Return true if cover is closed.""" if not (value := self._current_position_value) or value.value is None: return None - return bool(value.value == 0) + return bool(value.value == self._fully_closed_position) @property def current_cover_position(self) -> int | None: @@ -183,6 +200,7 @@ class CoverTiltMixin(ZWaveBaseEntity, CoverEntity): self, current_value: ZwaveValue, target_value: ZwaveValue | None = None, + stop_value: ZwaveValue | None = None, ) -> None: """Set values for tilt.""" self._attr_supported_features = ( @@ -196,6 +214,10 @@ class CoverTiltMixin(ZWaveBaseEntity, CoverEntity): TARGET_VALUE_PROPERTY, value_property_key=current_value.property_key ) + if stop_value: + self._stop_tilt_value = stop_value + self._attr_supported_features |= CoverEntityFeature.STOP_TILT + def percent_to_zwave_tilt(self, value: int) -> int: """Convert position in 0-100 scale to closed_value-open_value scale.""" return ( @@ -256,6 +278,12 @@ class CoverTiltMixin(ZWaveBaseEntity, CoverEntity): self._target_tilt_value, self._fully_closed_tilt ) + async def async_stop_cover_tilt(self, **kwargs: Any) -> None: + """Stop the cover tilt.""" + assert self._stop_tilt_value + # Stop the tilt, will stop regardless of the actual direction of travel. + await self.info.node.async_set_value(self._stop_tilt_value, False) + class ZWaveMultilevelSwitchCover(CoverPositionMixin): """Representation of a Z-Wave Cover that uses Multilevel Switch CC for position.""" @@ -304,6 +332,79 @@ class ZWaveTiltCover(ZWaveMultilevelSwitchCover, CoverTiltMixin): ) +class ZWaveWindowCovering(CoverPositionMixin, CoverTiltMixin): + """Representation of a Z-Wave Window Covering cover device.""" + + def __init__( + self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo + ) -> None: + """Initialize.""" + super().__init__(config_entry, driver, info) + pos_value: ZwaveValue | None = None + tilt_value: ZwaveValue | None = None + + # If primary value is for position, we have to search for a tilt value + if info.primary_value.property_key in COVER_POSITION_PROPERTY_KEYS: + pos_value = info.primary_value + tilt_value = next( + ( + value + for property_key in COVER_TILT_PROPERTY_KEYS + if ( + value := self.get_zwave_value( + CURRENT_VALUE_PROPERTY, value_property_key=property_key + ) + ) + ), + None, + ) + # If primary value is for tilt, there is no position value + else: + tilt_value = info.primary_value + + # Set position and tilt values if they exist. If the corresponding value is of + # the type No Position, we remove the corresponding set position feature. + for set_values_func, value, set_position_feature in ( + (self._set_position_values, pos_value, CoverEntityFeature.SET_POSITION), + (self._set_tilt_values, tilt_value, CoverEntityFeature.SET_TILT_POSITION), + ): + if value: + set_values_func( + value, + stop_value=self.get_zwave_value( + WINDOW_COVERING_OPEN_PROPERTY, + value_property_key=value.property_key, + ), + ) + if value.property_key in NO_POSITION_PROPERTY_KEYS: + assert self._attr_supported_features + self._attr_supported_features ^= set_position_feature + + additional_info: list[str] = [] + for value in (self._current_position_value, self._current_tilt_value): + if value and value.property_key_name: + additional_info.append( + value.property_key_name.removesuffix(f" {NO_POSITION_SUFFIX}") + ) + self._attr_name = self.generate_name(additional_info=additional_info) + self._attr_device_class = CoverDeviceClass.WINDOW + + @property + def _fully_open_tilt(self) -> int: + """Return position to open cover tilt.""" + return SlatStates.OPEN + + @property + def _fully_closed_tilt(self) -> int: + """Return position to close cover tilt.""" + return SlatStates.CLOSED_1 + + @property + def _tilt_range(self) -> int: + """Return range of valid tilt positions.""" + return abs(SlatStates.CLOSED_2 - SlatStates.CLOSED_1) + + class ZwaveMotorizedBarrier(ZWaveBaseEntity, CoverEntity): """Representation of a Z-Wave motorized barrier device.""" diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 7c49ee7f61b..2bd7210b043 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -48,7 +48,7 @@ from homeassistant.const import EntityCategory, Platform from homeassistant.core import callback from homeassistant.helpers.device_registry import DeviceEntry -from .const import LOGGER +from .const import COVER_POSITION_PROPERTY_KEYS, COVER_TILT_PROPERTY_KEYS, LOGGER from .discovery_data_template import ( BaseDiscoverySchemaDataTemplate, ConfigurableFanValueMappingDataTemplate, @@ -259,6 +259,18 @@ SIREN_TONE_SCHEMA = ZWaveValueDiscoverySchema( type={ValueType.NUMBER}, ) +WINDOW_COVERING_COVER_CURRENT_VALUE_SCHEMA = ZWaveValueDiscoverySchema( + command_class={CommandClass.WINDOW_COVERING}, + property={CURRENT_VALUE_PROPERTY}, + property_key=COVER_POSITION_PROPERTY_KEYS, +) + +WINDOW_COVERING_SLAT_CURRENT_VALUE_SCHEMA = ZWaveValueDiscoverySchema( + command_class={CommandClass.WINDOW_COVERING}, + property={CURRENT_VALUE_PROPERTY}, + property_key=COVER_TILT_PROPERTY_KEYS, +) + # For device class mapping see: # https://github.com/zwave-js/node-zwave-js/blob/master/packages/config/config/deviceClasses.json DISCOVERY_SCHEMAS = [ @@ -858,6 +870,17 @@ DISCOVERY_SCHEMAS = [ ), # cover # window coverings + ZWaveDiscoverySchema( + platform=Platform.COVER, + hint="window_covering", + primary_value=WINDOW_COVERING_COVER_CURRENT_VALUE_SCHEMA, + ), + ZWaveDiscoverySchema( + platform=Platform.COVER, + hint="window_covering", + primary_value=WINDOW_COVERING_SLAT_CURRENT_VALUE_SCHEMA, + absent_values=[WINDOW_COVERING_COVER_CURRENT_VALUE_SCHEMA], + ), ZWaveDiscoverySchema( platform=Platform.COVER, hint="multilevel_switch", @@ -869,6 +892,10 @@ DISCOVERY_SCHEMAS = [ "Multiposition Motor", }, primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, + absent_values=[ + WINDOW_COVERING_COVER_CURRENT_VALUE_SCHEMA, + WINDOW_COVERING_SLAT_CURRENT_VALUE_SCHEMA, + ], ), # cover # motorized barriers diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py index 6fe1a94b896..de6adda121a 100644 --- a/homeassistant/components/zwave_js/entity.py +++ b/homeassistant/components/zwave_js/entity.py @@ -1,6 +1,8 @@ """Generic Z-Wave Entity Class.""" from __future__ import annotations +from collections.abc import Sequence + from zwave_js_server.const import NodeStatus from zwave_js_server.model.driver import Driver from zwave_js_server.model.value import Value as ZwaveValue, get_value_id_str @@ -139,7 +141,7 @@ class ZWaveBaseEntity(Entity): self, include_value_name: bool = False, alternate_value_name: str | None = None, - additional_info: list[str | None] | None = None, + additional_info: Sequence[str | None] | None = None, name_prefix: str | None = None, ) -> str: """Generate entity name.""" diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 5b2417f258b..3191bbfcea5 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -445,6 +445,12 @@ def iblinds_v2_state_fixture(): return json.loads(load_fixture("zwave_js/cover_iblinds_v2_state.json")) +@pytest.fixture(name="iblinds_v3_state", scope="session") +def iblinds_v3_state_fixture(): + """Load the iBlinds v3 node state fixture data.""" + return json.loads(load_fixture("zwave_js/cover_iblinds_v3_state.json")) + + @pytest.fixture(name="qubino_shutter_state", scope="session") def qubino_shutter_state_fixture(): """Load the Qubino Shutter node state fixture data.""" @@ -953,6 +959,14 @@ def iblinds_v2_cover_fixture(client, iblinds_v2_state): return node +@pytest.fixture(name="iblinds_v3") +def iblinds_v3_cover_fixture(client, iblinds_v3_state): + """Mock an iBlinds v3 window cover node.""" + node = Node(client, copy.deepcopy(iblinds_v3_state)) + client.driver.controller.nodes[node.node_id] = node + return node + + @pytest.fixture(name="qubino_shutter") def qubino_shutter_cover_fixture(client, qubino_shutter_state): """Mock a Qubino flush shutter node.""" diff --git a/tests/components/zwave_js/fixtures/cover_iblinds_v3_state.json b/tests/components/zwave_js/fixtures/cover_iblinds_v3_state.json new file mode 100644 index 00000000000..f0da41e4b6f --- /dev/null +++ b/tests/components/zwave_js/fixtures/cover_iblinds_v3_state.json @@ -0,0 +1,1236 @@ +{ + "nodeId": 12, + "index": 0, + "installerIcon": 6656, + "userIcon": 6656, + "status": 4, + "ready": true, + "isListening": false, + "isRouting": true, + "isSecure": true, + "manufacturerId": 647, + "productId": 114, + "productType": 4, + "firmwareVersion": "3.12.1", + "zwavePlusVersion": 2, + "deviceConfig": { + "filename": "/data/db/devices/0x0287/iblindsv3.json", + "isEmbedded": true, + "manufacturer": "HAB Home Intelligence LLC", + "manufacturerId": 647, + "label": "iblinds V3", + "description": "Window Blind Controller", + "devices": [ + { + "productType": 4, + "productId": 113 + }, + { + "productType": 4, + "productId": 114 + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "preferred": false, + "associations": {}, + "paramInformation": { + "_map": {} + } + }, + "label": "iblinds V3", + "interviewAttempts": 1, + "endpoints": [ + { + "nodeId": 12, + "index": 0, + "installerIcon": 6656, + "userIcon": 6656, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 17, + "label": "Multilevel Switch" + }, + "specific": { + "key": 7, + "label": "Motor Control Class C" + }, + "mandatorySupportedCCs": [32, 38, 37, 114, 134], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 38, + "name": "Multilevel Switch", + "version": 4, + "isSecure": true + }, + { + "id": 37, + "name": "Binary Switch", + "version": 2, + "isSecure": true + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": true + }, + { + "id": 134, + "name": "Version", + "version": 3, + "isSecure": true + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": true + }, + { + "id": 89, + "name": "Association Group Information", + "version": 3, + "isSecure": true + }, + { + "id": 85, + "name": "Transport Service", + "version": 2, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": true + }, + { + "id": 115, + "name": "Powerlevel", + "version": 1, + "isSecure": true + }, + { + "id": 159, + "name": "Security 2", + "version": 1, + "isSecure": true + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 5, + "isSecure": true + }, + { + "id": 128, + "name": "Battery", + "version": 1, + "isSecure": true + }, + { + "id": 112, + "name": "Configuration", + "version": 4, + "isSecure": true + }, + { + "id": 135, + "name": "Indicator", + "version": 3, + "isSecure": true + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": true + }, + { + "id": 106, + "name": "Window Covering", + "version": 1, + "isSecure": true + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + } + ] + } + ], + "values": [ + { + "endpoint": 0, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 0, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 0, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 2, + "metadata": { + "type": "duration", + "readable": true, + "writeable": false, + "label": "Remaining duration", + "stateful": true, + "secret": false + }, + "value": { + "value": 0, + "unit": "seconds" + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 99, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 4, + "metadata": { + "type": "duration", + "readable": true, + "writeable": false, + "label": "Remaining duration", + "stateful": true, + "secret": false + }, + "value": { + "value": 0, + "unit": "seconds" + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current value", + "min": 0, + "max": 99, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Up", + "propertyName": "Up", + "ccVersion": 4, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Perform a level change (Up)", + "ccSpecific": { + "switchType": 2 + }, + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Down", + "propertyName": "Down", + "ccVersion": 4, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Perform a level change (Down)", + "ccSpecific": { + "switchType": 2 + }, + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "restorePrevious", + "propertyName": "restorePrevious", + "ccVersion": 4, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Restore previous value", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 106, + "commandClassName": "Window Covering", + "property": "currentValue", + "propertyKey": 23, + "propertyName": "currentValue", + "propertyKeyName": "Horizontal Slats Angle", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current value - Horizontal Slats Angle", + "ccSpecific": { + "parameter": 23 + }, + "min": 0, + "max": 99, + "states": { + "0": "Closed (up)", + "50": "Open", + "99": "Closed (down)" + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 106, + "commandClassName": "Window Covering", + "property": "targetValue", + "propertyKey": 23, + "propertyName": "targetValue", + "propertyKeyName": "Horizontal Slats Angle", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Target value - Horizontal Slats Angle", + "ccSpecific": { + "parameter": 23 + }, + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 99, + "states": { + "0": "Closed (up)", + "50": "Open", + "99": "Closed (down)" + }, + "stateful": true, + "secret": false + }, + "value": 99 + }, + { + "endpoint": 0, + "commandClass": 106, + "commandClassName": "Window Covering", + "property": "duration", + "propertyKey": 23, + "propertyName": "duration", + "propertyKeyName": "Horizontal Slats Angle", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": false, + "label": "Remaining duration - Horizontal Slats Angle", + "ccSpecific": { + "parameter": 23 + }, + "stateful": true, + "secret": false + }, + "value": { + "value": 0, + "unit": "seconds" + } + }, + { + "endpoint": 0, + "commandClass": 106, + "commandClassName": "Window Covering", + "property": "open", + "propertyKey": 23, + "propertyName": "open", + "propertyKeyName": "Horizontal Slats Angle", + "ccVersion": 0, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Open - Horizontal Slats Angle", + "ccSpecific": { + "parameter": 23 + }, + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + }, + "nodeId": 12, + "value": true + }, + { + "endpoint": 0, + "commandClass": 106, + "commandClassName": "Window Covering", + "property": "close0", + "propertyKey": 23, + "propertyName": "close0", + "propertyKeyName": "Horizontal Slats Angle", + "ccVersion": 0, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Close Up - Horizontal Slats Angle", + "ccSpecific": { + "parameter": 23 + }, + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 106, + "commandClassName": "Window Covering", + "property": "close99", + "propertyKey": 23, + "propertyName": "close99", + "propertyKeyName": "Horizontal Slats Angle", + "ccVersion": 0, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Close Down - Horizontal Slats Angle", + "ccSpecific": { + "parameter": 23 + }, + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + }, + "nodeId": 12, + "value": true + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 1, + "propertyName": "Close Interval", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Auto Calibration Tightness. Lower value = tighter, Higher value = looser (MUST BE RECALIBRATED).", + "label": "Close Interval", + "default": 22, + "min": 16, + "max": 32, + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 15 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 2, + "propertyName": "Reverse Direction", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Reverse Direction", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "No", + "1": "Yes" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 3, + "propertyName": "Send Reports", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Used to disable Z-Wave Reports. This is useful for systems that poll iblinds immediately after sending a position command. Disable this if an immediate report is causing iblinds motor to function improperly.", + "label": "Send Reports", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "Enable", + "1": "Disable" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 4, + "propertyName": "Default ON Value", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Value that iblinds will open to by default", + "label": "Default ON Value", + "default": 50, + "min": 0, + "max": 99, + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 50 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 5, + "propertyName": "Reset Button", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Disabling the reset button on the motor prevents it from accidentally being pressed", + "label": "Reset Button", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "Enable", + "1": "Disable" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 6, + "propertyName": "Movement Duration", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Movement Duration", + "default": 0, + "min": 0, + "max": 100, + "unit": "seconds", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 7, + "propertyName": "Remote Calibration", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "label": "Remote Calibration", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "Idle", + "1": "Start calibration" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 8, + "propertyName": "Minimum Tilt Level", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "label": "Minimum Tilt Level", + "default": 0, + "min": 0, + "max": 25, + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 9, + "propertyName": "Maximum Tilt Level", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "label": "Maximum Tilt Level", + "default": 99, + "min": 75, + "max": 99, + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 10, + "propertyName": "Override Response to ON Command", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "description": "Overrides Set commands with a value of 99 to use the value defined in parameter 4 (Default ON Value) instead", + "label": "Override Response to ON Command", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "Disable", + "1": "Enable" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Manufacturer ID", + "min": 0, + "max": 65535, + "stateful": true, + "secret": false + }, + "value": 647 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product type", + "min": 0, + "max": 65535, + "stateful": true, + "secret": false + }, + "value": 4 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product ID", + "min": 0, + "max": 65535, + "stateful": true, + "secret": false + }, + "value": 114 + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "level", + "propertyName": "level", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Battery level", + "min": 0, + "max": 100, + "unit": "%", + "stateful": true, + "secret": false + }, + "value": 90 + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "isLow", + "propertyName": "isLow", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Low battery level", + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Library type", + "states": { + "0": "Unknown", + "1": "Static Controller", + "2": "Controller", + "3": "Enhanced Slave", + "4": "Slave", + "5": "Installer", + "6": "Routing Slave", + "7": "Bridge Controller", + "8": "Device under Test", + "9": "N/A", + "10": "AV Remote", + "11": "AV Device" + }, + "stateful": true, + "secret": false + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version", + "stateful": true, + "secret": false + }, + "value": "7.12" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 3, + "metadata": { + "type": "string[]", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions", + "stateful": true, + "secret": false + }, + "value": ["3.12"] + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version", + "stateful": true, + "secret": false + }, + "value": 2 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "sdkVersion", + "propertyName": "sdkVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "SDK version", + "stateful": true, + "secret": false + }, + "value": "7.12.2" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationFrameworkAPIVersion", + "propertyName": "applicationFrameworkAPIVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave application framework API version", + "stateful": true, + "secret": false + }, + "value": "3.12.1" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationFrameworkBuildNumber", + "propertyName": "applicationFrameworkBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave application framework API build number", + "stateful": true, + "secret": false + }, + "value": 35 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hostInterfaceVersion", + "propertyName": "hostInterfaceVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Serial API version", + "stateful": true, + "secret": false + }, + "value": "unused" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hostInterfaceBuildNumber", + "propertyName": "hostInterfaceBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Serial API build number", + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "zWaveProtocolVersion", + "propertyName": "zWaveProtocolVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version", + "stateful": true, + "secret": false + }, + "value": "7.12.2" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "zWaveProtocolBuildNumber", + "propertyName": "zWaveProtocolBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol build number", + "stateful": true, + "secret": false + }, + "value": 35 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationVersion", + "propertyName": "applicationVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Application version", + "stateful": true, + "secret": false + }, + "value": "3.12.1" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationBuildNumber", + "propertyName": "applicationBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Application build number", + "stateful": true, + "secret": false + }, + "value": 43707 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 80, + "propertyKey": 3, + "propertyName": "Node Identify", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "Node Identify - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 80, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 80, + "propertyKey": 4, + "propertyName": "Node Identify", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "Node Identify - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 80, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 80, + "propertyKey": 5, + "propertyName": "Node Identify", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "Node Identify - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 80, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": "value", + "propertyName": "value", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Indicator value", + "ccSpecific": { + "indicatorId": 0 + }, + "min": 0, + "max": 255, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": "identify", + "propertyName": "identify", + "ccVersion": 3, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Identify", + "stateful": true, + "secret": false + } + } + ], + "isFrequentListening": "1000ms", + "maxDataRate": 100000, + "supportedDataRates": [40000, 100000], + "protocolVersion": 3, + "supportsBeaming": true, + "supportsSecurity": false, + "nodeType": 1, + "zwavePlusNodeType": 0, + "zwavePlusRoleType": 7, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 17, + "label": "Multilevel Switch" + }, + "specific": { + "key": 7, + "label": "Motor Control Class C" + }, + "mandatorySupportedCCs": [32, 38, 37, 114, 134], + "mandatoryControlledCCs": [] + }, + "interviewStage": "Complete", + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0287:0x0004:0x0072:3.12.1", + "statistics": { + "commandsTX": 109, + "commandsRX": 101, + "commandsDroppedRX": 2, + "commandsDroppedTX": 0, + "timeoutResponse": 8, + "rtt": 1217.2, + "rssi": -43, + "lwr": { + "protocolDataRate": 2, + "repeaters": [], + "rssi": -45, + "repeaterRSSI": [] + } + }, + "highestSecurityClass": 1, + "isControllerNode": false, + "keepAwake": false +} diff --git a/tests/components/zwave_js/test_cover.py b/tests/components/zwave_js/test_cover.py index 66992a5f4d0..ae38c82a75c 100644 --- a/tests/components/zwave_js/test_cover.py +++ b/tests/components/zwave_js/test_cover.py @@ -20,12 +20,15 @@ from homeassistant.components.cover import ( SERVICE_SET_COVER_POSITION, SERVICE_SET_COVER_TILT_POSITION, SERVICE_STOP_COVER, + SERVICE_STOP_COVER_TILT, CoverDeviceClass, + CoverEntityFeature, ) from homeassistant.components.zwave_js.helpers import ZwaveValueMatcher from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, STATE_CLOSED, STATE_CLOSING, STATE_OPEN, @@ -688,3 +691,107 @@ async def test_fibaro_fgr222_shutter_cover_no_tilt( assert state.state == STATE_UNKNOWN assert ATTR_CURRENT_POSITION not in state.attributes assert ATTR_CURRENT_TILT_POSITION not in state.attributes + + +async def test_iblinds_v3_cover( + hass: HomeAssistant, client, iblinds_v3, integration +) -> None: + """Test iBlinds v3 cover which uses Window Covering CC.""" + entity_id = "cover.window_blind_controller_horizontal_slats_angle" + state = hass.states.get(entity_id) + assert state + # This device has no state because there is no position value + assert state.state == STATE_UNKNOWN + assert state.attributes[ATTR_SUPPORTED_FEATURES] == ( + CoverEntityFeature.CLOSE_TILT + | CoverEntityFeature.OPEN_TILT + | CoverEntityFeature.SET_TILT_POSITION + | CoverEntityFeature.STOP_TILT + ) + assert ATTR_CURRENT_POSITION not in state.attributes + assert ATTR_CURRENT_TILT_POSITION in state.attributes + assert state.attributes[ATTR_CURRENT_TILT_POSITION] == 0 + + await hass.services.async_call( + DOMAIN, + SERVICE_CLOSE_COVER_TILT, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 12 + assert args["valueId"] == { + "endpoint": 0, + "commandClass": 106, + "property": "targetValue", + "propertyKey": 23, + } + assert args["value"] == 0 + + client.async_send_command.reset_mock() + + await hass.services.async_call( + DOMAIN, + SERVICE_OPEN_COVER_TILT, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 12 + assert args["valueId"] == { + "endpoint": 0, + "commandClass": 106, + "property": "targetValue", + "propertyKey": 23, + } + assert args["value"] == 50 + + client.async_send_command.reset_mock() + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_COVER_TILT_POSITION, + {ATTR_ENTITY_ID: entity_id, ATTR_TILT_POSITION: 12}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 12 + assert args["valueId"] == { + "endpoint": 0, + "commandClass": 106, + "property": "targetValue", + "propertyKey": 23, + } + assert args["value"] == 12 + + client.async_send_command.reset_mock() + + await hass.services.async_call( + DOMAIN, + SERVICE_STOP_COVER_TILT, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 12 + assert args["valueId"] == { + "endpoint": 0, + "commandClass": 106, + "property": "open", + "propertyKey": 23, + } + assert args["value"] is False + + client.async_send_command.reset_mock()