diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index e2bd69a1436..6cbb1ea3016 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -188,7 +188,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): ) self._set_modes_and_presets() self._attr_supported_features = 0 - if len(self._hvac_presets) > 1: + if self._current_mode and len(self._hvac_presets) > 1: self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE # If any setpoint value exists, we can assume temperature # can be set @@ -428,9 +428,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new target fan mode.""" - if not self._fan_mode: - return - + assert self._fan_mode is not None try: new_state = int( next( @@ -484,9 +482,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new target preset mode.""" - if self._current_mode is None: - # Thermostat(valve) has no support for setting a mode, so we make it a no-op - return + assert self._current_mode is not None if preset_mode == PRESET_NONE: # try to restore to the (translated) main hvac mode await self.async_set_hvac_mode(self.hvac_mode) diff --git a/homeassistant/components/zwave_js/cover.py b/homeassistant/components/zwave_js/cover.py index 30364d127eb..b3f3aeaf1c0 100644 --- a/homeassistant/components/zwave_js/cover.py +++ b/homeassistant/components/zwave_js/cover.py @@ -27,7 +27,6 @@ from homeassistant.components.cover import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -138,8 +137,7 @@ class ZWaveCover(ZWaveBaseEntity, CoverEntity): 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) - if target_value is None: - raise HomeAssistantError("Missing target value on device.") + assert target_value is not None await self.info.node.async_set_value( target_value, percent_to_zwave_position(kwargs[ATTR_POSITION]) ) @@ -147,15 +145,13 @@ class ZWaveCover(ZWaveBaseEntity, CoverEntity): async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" target_value = self.get_zwave_value(TARGET_VALUE_PROPERTY) - if target_value is None: - raise HomeAssistantError("Missing target value on device.") + assert target_value is not None await self.info.node.async_set_value(target_value, 99) async def async_close_cover(self, **kwargs: Any) -> None: """Close cover.""" target_value = self.get_zwave_value(TARGET_VALUE_PROPERTY) - if target_value is None: - raise HomeAssistantError("Missing target value on device.") + assert target_value is not None await self.info.node.async_set_value(target_value, 0) async def async_stop_cover(self, **kwargs: Any) -> None: diff --git a/homeassistant/components/zwave_js/number.py b/homeassistant/components/zwave_js/number.py index f898170e308..7f7f5d65bb8 100644 --- a/homeassistant/components/zwave_js/number.py +++ b/homeassistant/components/zwave_js/number.py @@ -113,10 +113,7 @@ class ZwaveVolumeNumberEntity(ZWaveBaseEntity, NumberEntity): super().__init__(config_entry, driver, info) max_value = cast(int, self.info.primary_value.metadata.max) min_value = cast(int, self.info.primary_value.metadata.min) - self.correction_factor = max_value - min_value - # Fallback in case we can't properly calculate correction factor - if self.correction_factor == 0: - self.correction_factor = 1 + self.correction_factor = (max_value - min_value) or 1 # Entity class attributes self._attr_native_min_value = 0 diff --git a/homeassistant/components/zwave_js/select.py b/homeassistant/components/zwave_js/select.py index 0360b968173..adb5820657f 100644 --- a/homeassistant/components/zwave_js/select.py +++ b/homeassistant/components/zwave_js/select.py @@ -11,7 +11,6 @@ from zwave_js_server.model.driver import Driver from homeassistant.components.select import DOMAIN as SELECT_DOMAIN, SelectEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -173,7 +172,6 @@ class ZwaveMultilevelSwitchSelectEntity(ZWaveBaseEntity, SelectEntity): async def async_select_option(self, option: str) -> None: """Change the selected option.""" - if (target_value := self._target_value) is None: - raise HomeAssistantError("Missing target value on device.") + assert self._target_value is not None key = next(key for key, val in self._lookup_map.items() if val == option) - await self.info.node.async_set_value(target_value, int(key)) + await self.info.node.async_set_value(self._target_value, int(key)) diff --git a/tests/components/zwave_js/test_cover.py b/tests/components/zwave_js/test_cover.py index 54f71fa00d3..f26b0d29069 100644 --- a/tests/components/zwave_js/test_cover.py +++ b/tests/components/zwave_js/test_cover.py @@ -1,5 +1,11 @@ """Test the Z-Wave JS cover platform.""" +from zwave_js_server.const import ( + CURRENT_STATE_PROPERTY, + CURRENT_VALUE_PROPERTY, + CommandClass, +) from zwave_js_server.event import Event +from zwave_js_server.model.node import Node from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, @@ -9,6 +15,7 @@ from homeassistant.components.cover import ( SERVICE_OPEN_COVER, CoverDeviceClass, ) +from homeassistant.components.zwave_js.helpers import ZwaveValueMatcher from homeassistant.const import ( ATTR_DEVICE_CLASS, STATE_CLOSED, @@ -18,6 +25,8 @@ from homeassistant.const import ( STATE_UNKNOWN, ) +from .common import replace_value_of_zwave_value + WINDOW_COVER_ENTITY = "cover.zws_12" GDC_COVER_ENTITY = "cover.aeon_labs_garage_door_controller_gen5" BLIND_COVER_ENTITY = "cover.window_blind_controller" @@ -600,3 +609,59 @@ async def test_motor_barrier_cover(hass, client, gdc_zw062, integration): state = hass.states.get(GDC_COVER_ENTITY) assert state.state == STATE_UNKNOWN + + +async def test_motor_barrier_cover_no_primary_value( + hass, client, gdc_zw062_state, integration +): + """Test the cover entity where primary value value is None.""" + node_state = replace_value_of_zwave_value( + gdc_zw062_state, + [ + ZwaveValueMatcher( + property_=CURRENT_STATE_PROPERTY, + command_class=CommandClass.BARRIER_OPERATOR, + ) + ], + None, + ) + node = Node(client, node_state) + client.driver.controller.emit("node added", {"node": node}) + await hass.async_block_till_done() + + state = hass.states.get(GDC_COVER_ENTITY) + assert state + assert state.attributes[ATTR_DEVICE_CLASS] == CoverDeviceClass.GARAGE + + assert state.state == STATE_UNKNOWN + assert ATTR_CURRENT_POSITION not in state.attributes + + +async def test_fibaro_FGR222_shutter_cover_no_tilt( + hass, client, fibaro_fgr222_shutter_state, integration +): + """Test tilt function of the Fibaro Shutter devices with tilt value is None.""" + node_state = replace_value_of_zwave_value( + fibaro_fgr222_shutter_state, + [ + ZwaveValueMatcher( + property_="fibaro", + command_class=CommandClass.MANUFACTURER_PROPRIETARY, + property_key="venetianBlindsTilt", + ), + ZwaveValueMatcher( + property_=CURRENT_VALUE_PROPERTY, + command_class=CommandClass.SWITCH_MULTILEVEL, + ), + ], + None, + ) + node = Node(client, node_state) + client.driver.controller.emit("node added", {"node": node}) + await hass.async_block_till_done() + + state = hass.states.get(FIBARO_SHUTTER_COVER_ENTITY) + assert state + assert state.state == STATE_UNKNOWN + assert ATTR_CURRENT_POSITION not in state.attributes + assert ATTR_CURRENT_TILT_POSITION not in state.attributes diff --git a/tests/components/zwave_js/test_lock.py b/tests/components/zwave_js/test_lock.py index 5f35a568f37..03ebd3b6453 100644 --- a/tests/components/zwave_js/test_lock.py +++ b/tests/components/zwave_js/test_lock.py @@ -1,7 +1,12 @@ """Test the Z-Wave JS lock platform.""" -from zwave_js_server.const.command_class.lock import ATTR_CODE_SLOT, ATTR_USERCODE +from zwave_js_server.const import CommandClass +from zwave_js_server.const.command_class.lock import ( + ATTR_CODE_SLOT, + ATTR_USERCODE, + CURRENT_MODE_PROPERTY, +) from zwave_js_server.event import Event -from zwave_js_server.model.node import NodeStatus +from zwave_js_server.model.node import Node, NodeStatus from homeassistant.components.lock import ( DOMAIN as LOCK_DOMAIN, @@ -9,6 +14,7 @@ from homeassistant.components.lock import ( SERVICE_UNLOCK, ) from homeassistant.components.zwave_js.const import DOMAIN as ZWAVE_JS_DOMAIN +from homeassistant.components.zwave_js.helpers import ZwaveValueMatcher from homeassistant.components.zwave_js.lock import ( SERVICE_CLEAR_LOCK_USERCODE, SERVICE_SET_LOCK_USERCODE, @@ -17,10 +23,11 @@ from homeassistant.const import ( ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNAVAILABLE, + STATE_UNKNOWN, STATE_UNLOCKED, ) -from .common import SCHLAGE_BE469_LOCK_ENTITY +from .common import SCHLAGE_BE469_LOCK_ENTITY, replace_value_of_zwave_value async def test_door_lock(hass, client, lock_schlage_be469, integration): @@ -160,3 +167,23 @@ async def test_door_lock(hass, client, lock_schlage_be469, integration): async def test_only_one_lock(hass, client, lock_home_connect_620, integration): """Test node with both Door Lock and Lock CC values only gets one lock entity.""" assert len(hass.states.async_entity_ids("lock")) == 1 + + +async def test_door_lock_no_value(hass, client, lock_schlage_be469_state, integration): + """Test a lock entity with door lock command class that has no value for mode.""" + node_state = replace_value_of_zwave_value( + lock_schlage_be469_state, + [ + ZwaveValueMatcher( + property_=CURRENT_MODE_PROPERTY, + command_class=CommandClass.DOOR_LOCK, + ) + ], + None, + ) + node = Node(client, node_state) + client.driver.controller.emit("node added", {"node": node}) + await hass.async_block_till_done() + state = hass.states.get(SCHLAGE_BE469_LOCK_ENTITY) + assert state + assert state.state == STATE_UNKNOWN diff --git a/tests/components/zwave_js/test_select.py b/tests/components/zwave_js/test_select.py index 1cf5fb54304..e278c11ca72 100644 --- a/tests/components/zwave_js/test_select.py +++ b/tests/components/zwave_js/test_select.py @@ -1,15 +1,19 @@ """Test the Z-Wave JS number platform.""" from unittest.mock import MagicMock +from zwave_js_server.const import CURRENT_VALUE_PROPERTY, CommandClass from zwave_js_server.event import Event from zwave_js_server.model.node import Node +from homeassistant.components.zwave_js.helpers import ZwaveValueMatcher from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory import homeassistant.helpers.entity_registry as er +from .common import replace_value_of_zwave_value + DEFAULT_TONE_SELECT_ENTITY = "select.indoor_siren_6_default_tone_2" PROTECTION_SELECT_ENTITY = "select.family_room_combo_local_protection_state" MULTILEVEL_SWITCH_SELECT_ENTITY = "select.front_door_siren" @@ -265,3 +269,27 @@ async def test_multilevel_switch_select(hass, client, fortrezz_ssa1_siren, integ state = hass.states.get(MULTILEVEL_SWITCH_SELECT_ENTITY) assert state.state == "Strobe ONLY" + + +async def test_multilevel_switch_select_no_value( + hass, client, fortrezz_ssa1_siren_state, integration +): + """Test Multilevel Switch CC based select entity with primary value is None.""" + node_state = replace_value_of_zwave_value( + fortrezz_ssa1_siren_state, + [ + ZwaveValueMatcher( + property_=CURRENT_VALUE_PROPERTY, + command_class=CommandClass.SWITCH_MULTILEVEL, + ) + ], + None, + ) + node = Node(client, node_state) + client.driver.controller.emit("node added", {"node": node}) + await hass.async_block_till_done() + + state = hass.states.get(MULTILEVEL_SWITCH_SELECT_ENTITY) + + assert state + assert state.state == STATE_UNKNOWN diff --git a/tests/components/zwave_js/test_switch.py b/tests/components/zwave_js/test_switch.py index b84ab32f618..d485c877cc4 100644 --- a/tests/components/zwave_js/test_switch.py +++ b/tests/components/zwave_js/test_switch.py @@ -1,11 +1,14 @@ """Test the Z-Wave JS switch platform.""" +from zwave_js_server.const import CURRENT_VALUE_PROPERTY, CommandClass from zwave_js_server.event import Event +from zwave_js_server.model.node import Node from homeassistant.components.switch import DOMAIN, SERVICE_TURN_OFF, SERVICE_TURN_ON -from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.components.zwave_js.helpers import ZwaveValueMatcher +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN -from .common import SWITCH_ENTITY +from .common import SWITCH_ENTITY, replace_value_of_zwave_value async def test_switch(hass, hank_binary_switch, integration, client): @@ -14,7 +17,7 @@ async def test_switch(hass, hank_binary_switch, integration, client): node = hank_binary_switch assert state - assert state.state == "off" + assert state.state == STATE_OFF # Test turning on await hass.services.async_call( @@ -178,3 +181,25 @@ async def test_barrier_signaling_switch(hass, gdc_zw062, integration, client): state = hass.states.get(entity) assert state.state == STATE_ON + + +async def test_switch_no_value(hass, hank_binary_switch_state, integration, client): + """Test the switch where primary value value is None.""" + node_state = replace_value_of_zwave_value( + hank_binary_switch_state, + [ + ZwaveValueMatcher( + property_=CURRENT_VALUE_PROPERTY, + command_class=CommandClass.SWITCH_BINARY, + ) + ], + None, + ) + node = Node(client, node_state) + client.driver.controller.emit("node added", {"node": node}) + await hass.async_block_till_done() + + state = hass.states.get(SWITCH_ENTITY) + + assert state + assert state.state == STATE_UNKNOWN