diff --git a/homeassistant/components/zwave_js/cover.py b/homeassistant/components/zwave_js/cover.py index 686a186a7cbc45..999c59cc23960e 100644 --- a/homeassistant/components/zwave_js/cover.py +++ b/homeassistant/components/zwave_js/cover.py @@ -66,36 +66,6 @@ def async_add_cover(info: ZwaveDiscoveryInfo) -> None: ) -def percent_to_zwave_position(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 max(1, round((value / 100) * 99)) - return 0 - - -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.""" @@ -130,13 +100,41 @@ def __init__( 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. + + `value` -- (int) Position byte value from 0-100. + """ + if value > self._fully_closed_value: + return max( + 1, round((value / 100) * self._cover_range) + self._fully_closed_value + ) + return self._fully_closed_value + + @property + def _fully_open_value(self) -> int: + """Return value that represents fully opened.""" + max_ = self.info.primary_value.metadata.max + return 99 if max_ is None else max_ + + @property + def _fully_closed_value(self) -> int: + """Return value that represents fully closed.""" + min_ = self.info.primary_value.metadata.min + return 0 if min_ is None else min_ + + @property + def _cover_range(self) -> int: + """Return range between fully opened and fully closed.""" + return self._fully_open_value - self._fully_closed_value + @property def is_closed(self) -> bool | None: """Return true if cover is closed.""" if self.info.primary_value.value is None: # guard missing value return None - return bool(self.info.primary_value.value == 0) + return bool(self.info.primary_value.value == self._fully_closed_value) @property def current_cover_position(self) -> int | None: @@ -144,27 +142,33 @@ def current_cover_position(self) -> int | None: if self.info.primary_value.value is None: # guard missing value return None - return round((cast(int, self.info.primary_value.value) / 99) * 100) + return round( + ( + (cast(int, self.info.primary_value.value) - self._fully_closed_value) + / self._cover_range + ) + * 100 + ) 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 await self.info.node.async_set_value( - target_value, percent_to_zwave_position(kwargs[ATTR_POSITION]) + target_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) + await self.info.node.async_set_value(target_value, self._fully_open_value) 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) + await self.info.node.async_set_value(target_value, self._fully_closed_value) async def async_stop_cover(self, **kwargs: Any) -> None: """Stop cover.""" @@ -195,6 +199,24 @@ def __init__( | CoverEntityFeature.SET_TILT_POSITION ) + def percent_to_zwave_tilt(self, value: int) -> int: + """Convert position in 0-100 scale to closed_value-open_value scale. + + `value` -- (int) Position byte value from 0-100. + """ + if value > self._fully_closed_value: + return round((value / 100) * self._cover_range) + self._fully_closed_value + return self._fully_closed_value + + def zwave_tilt_to_percent(self, value: int) -> int: + """Convert closed_value-open_value scale to position in 0-100 scale. + + `value` -- (int) Position byte value from closed_value-open_value. + """ + if value > self._fully_closed_value: + return round(((value - self._fully_closed_value) / self._cover_range) * 100) + return self._fully_closed_value + @property def current_cover_tilt_position(self) -> int | None: """Return current position of cover tilt. @@ -204,14 +226,14 @@ def current_cover_tilt_position(self) -> int | None: value = self._current_tilt_value if value is None or value.value is None: return None - return zwave_tilt_to_percent(int(value.value)) + return self.zwave_tilt_to_percent(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 await self.info.node.async_set_value( self._current_tilt_value, - percent_to_zwave_tilt(kwargs[ATTR_TILT_POSITION]), + self.percent_to_zwave_tilt(kwargs[ATTR_TILT_POSITION]), ) async def async_open_cover_tilt(self, **kwargs: Any) -> None: diff --git a/tests/components/zwave_js/test_cover.py b/tests/components/zwave_js/test_cover.py index f1e9366593807e..bb4fe23d265fe1 100644 --- a/tests/components/zwave_js/test_cover.py +++ b/tests/components/zwave_js/test_cover.py @@ -10,14 +10,20 @@ from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, ATTR_CURRENT_TILT_POSITION, + ATTR_POSITION, DOMAIN, SERVICE_CLOSE_COVER, + SERVICE_CLOSE_COVER_TILT, SERVICE_OPEN_COVER, + SERVICE_OPEN_COVER_TILT, + SERVICE_SET_COVER_POSITION, + SERVICE_STOP_COVER, CoverDeviceClass, ) from homeassistant.components.zwave_js.helpers import ZwaveValueMatcher from homeassistant.const import ( ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, STATE_CLOSED, STATE_CLOSING, STATE_OPEN, @@ -46,14 +52,14 @@ async def test_window_cover( assert state assert state.attributes[ATTR_DEVICE_CLASS] == CoverDeviceClass.WINDOW - assert state.state == "closed" + assert state.state == STATE_CLOSED assert state.attributes[ATTR_CURRENT_POSITION] == 0 # Test setting position await hass.services.async_call( - "cover", - "set_cover_position", - {"entity_id": WINDOW_COVER_ENTITY, "position": 50}, + DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: WINDOW_COVER_ENTITY, ATTR_POSITION: 50}, blocking=True, ) @@ -72,9 +78,9 @@ async def test_window_cover( # Test setting position await hass.services.async_call( - "cover", - "set_cover_position", - {"entity_id": WINDOW_COVER_ENTITY, "position": 0}, + DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: WINDOW_COVER_ENTITY, ATTR_POSITION: 0}, blocking=True, ) @@ -93,9 +99,9 @@ async def test_window_cover( # Test opening await hass.services.async_call( - "cover", - "open_cover", - {"entity_id": WINDOW_COVER_ENTITY}, + DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: WINDOW_COVER_ENTITY}, blocking=True, ) @@ -113,9 +119,9 @@ async def test_window_cover( client.async_send_command.reset_mock() # Test stop after opening await hass.services.async_call( - "cover", - "stop_cover", - {"entity_id": WINDOW_COVER_ENTITY}, + DOMAIN, + SERVICE_STOP_COVER, + {ATTR_ENTITY_ID: WINDOW_COVER_ENTITY}, blocking=True, ) @@ -152,13 +158,13 @@ async def test_window_cover( client.async_send_command.reset_mock() state = hass.states.get(WINDOW_COVER_ENTITY) - assert state.state == "open" + assert state.state == STATE_OPEN # Test closing await hass.services.async_call( - "cover", - "close_cover", - {"entity_id": WINDOW_COVER_ENTITY}, + DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: WINDOW_COVER_ENTITY}, blocking=True, ) assert len(client.async_send_command.call_args_list) == 1 @@ -176,9 +182,9 @@ async def test_window_cover( # Test stop after closing await hass.services.async_call( - "cover", - "stop_cover", - {"entity_id": WINDOW_COVER_ENTITY}, + DOMAIN, + SERVICE_STOP_COVER, + {ATTR_ENTITY_ID: WINDOW_COVER_ENTITY}, blocking=True, ) @@ -215,10 +221,10 @@ async def test_window_cover( node.receive_event(event) state = hass.states.get(WINDOW_COVER_ENTITY) - assert state.state == "closed" + assert state.state == STATE_CLOSED -async def test_fibaro_FGR222_shutter_cover( +async def test_fibaro_fgr222_shutter_cover( hass: HomeAssistant, client, fibaro_fgr222_shutter, integration ) -> None: """Test tilt function of the Fibaro Shutter devices.""" @@ -226,14 +232,14 @@ async def test_fibaro_FGR222_shutter_cover( assert state assert state.attributes[ATTR_DEVICE_CLASS] == CoverDeviceClass.SHUTTER - assert state.state == "open" + assert state.state == STATE_OPEN assert state.attributes[ATTR_CURRENT_TILT_POSITION] == 0 # Test opening tilts await hass.services.async_call( - "cover", - "open_cover_tilt", - {"entity_id": FIBARO_SHUTTER_COVER_ENTITY}, + DOMAIN, + SERVICE_OPEN_COVER_TILT, + {ATTR_ENTITY_ID: FIBARO_SHUTTER_COVER_ENTITY}, blocking=True, ) @@ -252,9 +258,9 @@ async def test_fibaro_FGR222_shutter_cover( client.async_send_command.reset_mock() # Test closing tilts await hass.services.async_call( - "cover", - "close_cover_tilt", - {"entity_id": FIBARO_SHUTTER_COVER_ENTITY}, + DOMAIN, + SERVICE_CLOSE_COVER_TILT, + {ATTR_ENTITY_ID: FIBARO_SHUTTER_COVER_ENTITY}, blocking=True, ) @@ -306,14 +312,14 @@ async def test_aeotec_nano_shutter_cover( assert state assert state.attributes[ATTR_DEVICE_CLASS] == CoverDeviceClass.WINDOW - assert state.state == "closed" + assert state.state == STATE_CLOSED assert state.attributes[ATTR_CURRENT_POSITION] == 0 # Test opening await hass.services.async_call( - "cover", - "open_cover", - {"entity_id": AEOTEC_SHUTTER_COVER_ENTITY}, + DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: AEOTEC_SHUTTER_COVER_ENTITY}, blocking=True, ) @@ -331,9 +337,9 @@ async def test_aeotec_nano_shutter_cover( client.async_send_command.reset_mock() # Test stop after opening await hass.services.async_call( - "cover", - "stop_cover", - {"entity_id": AEOTEC_SHUTTER_COVER_ENTITY}, + DOMAIN, + SERVICE_STOP_COVER, + {ATTR_ENTITY_ID: AEOTEC_SHUTTER_COVER_ENTITY}, blocking=True, ) @@ -371,13 +377,13 @@ async def test_aeotec_nano_shutter_cover( client.async_send_command.reset_mock() state = hass.states.get(AEOTEC_SHUTTER_COVER_ENTITY) - assert state.state == "open" + assert state.state == STATE_OPEN # Test closing await hass.services.async_call( - "cover", - "close_cover", - {"entity_id": AEOTEC_SHUTTER_COVER_ENTITY}, + DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: AEOTEC_SHUTTER_COVER_ENTITY}, blocking=True, ) assert len(client.async_send_command.call_args_list) == 1 @@ -395,9 +401,9 @@ async def test_aeotec_nano_shutter_cover( # Test stop after closing await hass.services.async_call( - "cover", - "stop_cover", - {"entity_id": AEOTEC_SHUTTER_COVER_ENTITY}, + DOMAIN, + SERVICE_STOP_COVER, + {ATTR_ENTITY_ID: AEOTEC_SHUTTER_COVER_ENTITY}, blocking=True, ) @@ -447,7 +453,7 @@ async def test_motor_barrier_cover( # Test open await hass.services.async_call( - DOMAIN, SERVICE_OPEN_COVER, {"entity_id": GDC_COVER_ENTITY}, blocking=True + DOMAIN, SERVICE_OPEN_COVER, {ATTR_ENTITY_ID: GDC_COVER_ENTITY}, blocking=True ) assert len(client.async_send_command.call_args_list) == 1 @@ -469,7 +475,7 @@ async def test_motor_barrier_cover( # Test close await hass.services.async_call( - DOMAIN, SERVICE_CLOSE_COVER, {"entity_id": GDC_COVER_ENTITY}, blocking=True + DOMAIN, SERVICE_CLOSE_COVER, {ATTR_ENTITY_ID: GDC_COVER_ENTITY}, blocking=True ) assert len(client.async_send_command.call_args_list) == 1 @@ -631,7 +637,7 @@ async def test_motor_barrier_cover_no_primary_value( assert ATTR_CURRENT_POSITION not in state.attributes -async def test_fibaro_FGR222_shutter_cover_no_tilt( +async def test_fibaro_fgr222_shutter_cover_no_tilt( hass: HomeAssistant, client, fibaro_fgr222_shutter_state, integration ) -> None: """Test tilt function of the Fibaro Shutter devices with tilt value is None."""