diff --git a/homeassistant/components/zwave_js/binary_sensor.py b/homeassistant/components/zwave_js/binary_sensor.py index 7e9882377bd..fb085cffe62 100644 --- a/homeassistant/components/zwave_js/binary_sensor.py +++ b/homeassistant/components/zwave_js/binary_sensor.py @@ -248,7 +248,7 @@ PROPERTY_SENSOR_MAPPINGS: dict[str, PropertyZWaveJSEntityDescription] = { # Mappings for boolean sensors -BOOLEAN_SENSOR_MAPPINGS: dict[str, BinarySensorEntityDescription] = { +BOOLEAN_SENSOR_MAPPINGS: dict[int, BinarySensorEntityDescription] = { CommandClass.BATTERY: BinarySensorEntityDescription( key=str(CommandClass.BATTERY), device_class=BinarySensorDeviceClass.BATTERY, @@ -304,9 +304,13 @@ async def async_setup_entry( config_entry, driver, info, state_key, notification_description ) ) - elif info.platform_hint == "property" and ( - property_description := PROPERTY_SENSOR_MAPPINGS.get( - info.primary_value.property_name + elif ( + info.platform_hint == "property" + and info.primary_value.property_name + and ( + property_description := PROPERTY_SENSOR_MAPPINGS.get( + info.primary_value.property_name + ) ) ): entities.append( diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index f721db41d9f..cad0b6a1e5c 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -138,7 +138,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): self._current_mode = self.get_zwave_value( THERMOSTAT_MODE_PROPERTY, command_class=CommandClass.THERMOSTAT_MODE ) - self._setpoint_values: dict[ThermostatSetpointType, ZwaveValue] = {} + self._setpoint_values: dict[ThermostatSetpointType, ZwaveValue | None] = {} for enum in ThermostatSetpointType: self._setpoint_values[enum] = self.get_zwave_value( THERMOSTAT_SETPOINT_PROPERTY, @@ -233,9 +233,9 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): self._hvac_presets = all_presets @property - def _current_mode_setpoint_enums(self) -> list[ThermostatSetpointType | None]: + def _current_mode_setpoint_enums(self) -> list[ThermostatSetpointType]: """Return the list of enums that are relevant to the current thermostat mode.""" - if self._current_mode is None: + if self._current_mode is None or self._current_mode.value is None: # Thermostat(valve) with no support for setting a mode is considered heating-only return [ThermostatSetpointType.HEATING] return THERMOSTAT_MODE_SETPOINT_MAP.get(int(self._current_mode.value), []) # type: ignore[no-any-return] @@ -329,12 +329,13 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): @property def preset_mode(self) -> str | None: """Return the current preset mode, e.g., home, away, temp.""" - if self._current_mode and self._current_mode.value is None: + if self._current_mode is None or self._current_mode.value is None: # guard missing value return None - if self._current_mode and int(self._current_mode.value) not in THERMOSTAT_MODES: - return_val: str = self._current_mode.metadata.states.get( - str(self._current_mode.value) + if int(self._current_mode.value) not in THERMOSTAT_MODES: + return_val: str = cast( + str, + self._current_mode.metadata.states.get(str(self._current_mode.value)), ) return return_val return PRESET_NONE @@ -468,6 +469,9 @@ 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 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 f9a9990ed77..ee83db4578c 100644 --- a/homeassistant/components/zwave_js/cover.py +++ b/homeassistant/components/zwave_js/cover.py @@ -28,6 +28,7 @@ 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 @@ -140,6 +141,8 @@ 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.") await self.info.node.async_set_value( target_value, percent_to_zwave_position(kwargs[ATTR_POSITION]) ) @@ -147,11 +150,15 @@ 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.") 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.") await self.info.node.async_set_value(target_value, 0) async def async_stop_cover(self, **kwargs: Any) -> None: @@ -207,7 +214,9 @@ class ZWaveTiltCover(ZWaveCover): None is unknown, 0 is closed, 100 is fully open. """ value = self.data_template.current_tilt_value(self.info.platform_data) - return zwave_tilt_to_percent(value.value) if value else None + if value is None or value.value is None: + return None + return 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.""" @@ -241,8 +250,10 @@ class ZwaveMotorizedBarrier(ZWaveBaseEntity, CoverEntity): ) -> None: """Initialize a ZwaveMotorizedBarrier entity.""" super().__init__(config_entry, driver, info) - self._target_state: ZwaveValue = self.get_zwave_value( - TARGET_STATE_PROPERTY, add_to_watched_value_ids=False + # TARGET_STATE_PROPERTY is required in the discovery schema. + self._target_state = cast( + ZwaveValue, + self.get_zwave_value(TARGET_STATE_PROPERTY, add_to_watched_value_ids=False), ) @property diff --git a/homeassistant/components/zwave_js/fan.py b/homeassistant/components/zwave_js/fan.py index eb6d053f958..ae9df47b420 100644 --- a/homeassistant/components/zwave_js/fan.py +++ b/homeassistant/components/zwave_js/fan.py @@ -96,7 +96,9 @@ class ZwaveFan(ZWaveBaseEntity, FanEntity): percentage_to_ranged_value(DEFAULT_SPEED_RANGE, percentage) ) - await self.info.node.async_set_value(self._target_value, zwave_speed) + if (target_value := self._target_value) is None: + raise HomeAssistantError("Missing target value on device.") + await self.info.node.async_set_value(target_value, zwave_speed) async def async_turn_on( self, @@ -110,12 +112,16 @@ class ZwaveFan(ZWaveBaseEntity, FanEntity): elif preset_mode is not None: await self.async_set_preset_mode(preset_mode) else: + if (target_value := self._target_value) is None: + raise HomeAssistantError("Missing target value on device.") # Value 255 tells device to return to previous value - await self.info.node.async_set_value(self._target_value, 255) + await self.info.node.async_set_value(target_value, 255) async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" - await self.info.node.async_set_value(self._target_value, 0) + if (target_value := self._target_value) is None: + raise HomeAssistantError("Missing target value on device.") + await self.info.node.async_set_value(target_value, 0) @property def is_on(self) -> bool | None: @@ -160,14 +166,18 @@ class ValueMappingZwaveFan(ZwaveFan): async def async_set_percentage(self, percentage: int) -> None: """Set the speed percentage of the fan.""" + if (target_value := self._target_value) is None: + raise HomeAssistantError("Missing target value on device.") zwave_speed = self.percentage_to_zwave_speed(percentage) - await self.info.node.async_set_value(self._target_value, zwave_speed) + await self.info.node.async_set_value(target_value, zwave_speed) async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" + if (target_value := self._target_value) is None: + raise HomeAssistantError("Missing target value on device.") for zwave_value, mapped_preset_mode in self.fan_value_mapping.presets.items(): if preset_mode == mapped_preset_mode: - await self.info.node.async_set_value(self._target_value, zwave_value) + await self.info.node.async_set_value(target_value, zwave_value) return raise NotValidPresetModeError( @@ -210,7 +220,9 @@ class ValueMappingZwaveFan(ZwaveFan): @property def preset_mode(self) -> str | None: """Return the current preset mode.""" - return self.fan_value_mapping.presets.get(self.info.primary_value.value) + if (value := self.info.primary_value.value) is None: + return None + return self.fan_value_mapping.presets.get(value) @property def has_fan_value_mapping(self) -> bool: diff --git a/homeassistant/components/zwave_js/light.py b/homeassistant/components/zwave_js/light.py index d026868b418..17293e85a21 100644 --- a/homeassistant/components/zwave_js/light.py +++ b/homeassistant/components/zwave_js/light.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Any +from typing import Any, cast from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import ( @@ -24,6 +24,7 @@ from zwave_js_server.const.command_class.color_switch import ( ColorComponent, ) from zwave_js_server.model.driver import Driver +from zwave_js_server.model.value import Value from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -301,10 +302,14 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): """Set (multiple) defined colors to given value(s).""" # prefer the (new) combined color property # https://github.com/zwave-js/node-zwave-js/pull/1782 - combined_color_val = self.get_zwave_value( - "targetColor", - CommandClass.SWITCH_COLOR, - value_property_key=None, + # Setting colors is only done if there's a target color value. + combined_color_val = cast( + Value, + self.get_zwave_value( + "targetColor", + CommandClass.SWITCH_COLOR, + value_property_key=None, + ), ) zwave_transition = None diff --git a/homeassistant/components/zwave_js/lock.py b/homeassistant/components/zwave_js/lock.py index e7fbbeb3f99..ffe99373991 100644 --- a/homeassistant/components/zwave_js/lock.py +++ b/homeassistant/components/zwave_js/lock.py @@ -14,7 +14,6 @@ from zwave_js_server.const.command_class.lock import ( LOCK_CMD_CLASS_TO_PROPERTY_MAP, DoorLockMode, ) -from zwave_js_server.model.value import Value as ZwaveValue from zwave_js_server.util.lock import clear_usercode, set_usercode from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN, LockEntity @@ -111,8 +110,10 @@ class ZWaveLock(ZWaveBaseEntity, LockEntity): async def _set_lock_state(self, target_state: str, **kwargs: Any) -> None: """Set the lock state.""" - target_value: ZwaveValue = self.get_zwave_value( - LOCK_CMD_CLASS_TO_PROPERTY_MAP[self.info.primary_value.command_class] + target_value = self.get_zwave_value( + LOCK_CMD_CLASS_TO_PROPERTY_MAP[ + CommandClass(self.info.primary_value.command_class) + ] ) if target_value is not None: await self.info.node.async_set_value( diff --git a/homeassistant/components/zwave_js/number.py b/homeassistant/components/zwave_js/number.py index 56a7ce33be6..737b872b7bc 100644 --- a/homeassistant/components/zwave_js/number.py +++ b/homeassistant/components/zwave_js/number.py @@ -1,13 +1,17 @@ """Support for Z-Wave controls using the number platform.""" from __future__ import annotations +from typing import cast + from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import TARGET_VALUE_PROPERTY from zwave_js_server.model.driver import Driver +from zwave_js_server.model.value import Value from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN, NumberEntity 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 @@ -55,6 +59,7 @@ class ZwaveNumberEntity(ZWaveBaseEntity, NumberEntity): ) -> None: """Initialize a ZwaveNumberEntity entity.""" super().__init__(config_entry, driver, info) + self._target_value: Value | None if self.info.primary_value.metadata.writeable: self._target_value = self.info.primary_value else: @@ -95,7 +100,9 @@ class ZwaveNumberEntity(ZWaveBaseEntity, NumberEntity): async def async_set_value(self, value: float) -> None: """Set new value.""" - await self.info.node.async_set_value(self._target_value, value) + if (target_value := self._target_value) is None: + raise HomeAssistantError("Missing target value on device.") + await self.info.node.async_set_value(target_value, value) class ZwaveVolumeNumberEntity(ZWaveBaseEntity, NumberEntity): @@ -106,9 +113,9 @@ class ZwaveVolumeNumberEntity(ZWaveBaseEntity, NumberEntity): ) -> None: """Initialize a ZwaveVolumeNumberEntity entity.""" super().__init__(config_entry, driver, info) - self.correction_factor = int( - self.info.primary_value.metadata.max - self.info.primary_value.metadata.min - ) + 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 diff --git a/homeassistant/components/zwave_js/select.py b/homeassistant/components/zwave_js/select.py index f8cb294919e..f61149e5de7 100644 --- a/homeassistant/components/zwave_js/select.py +++ b/homeassistant/components/zwave_js/select.py @@ -11,6 +11,7 @@ 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 @@ -174,5 +175,7 @@ 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.") key = next(key for key, val in self._lookup_map.items() if val == option) - await self.info.node.async_set_value(self._target_value, int(key)) + await self.info.node.async_set_value(target_value, int(key)) diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index eb5bf778cc0..2b2e2a0de2b 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -26,6 +26,7 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo, EntityCategory @@ -224,7 +225,7 @@ async def async_setup_entry( LOGGER.warning( "Sensor not implemented for %s/%s", info.platform_hint, - info.primary_value.propertyname, + info.primary_value.property_name, ) return @@ -352,13 +353,15 @@ class ZWaveMeterSensor(ZWaveNumericSensor): """Reset meter(s) on device.""" node = self.info.node primary_value = self.info.primary_value + if (endpoint := primary_value.endpoint) is None: + raise HomeAssistantError("Missing endpoint on device.") options = {} if meter_type is not None: options[RESET_METER_OPTION_TYPE] = meter_type if value is not None: options[RESET_METER_OPTION_TARGET_VALUE] = value args = [options] if options else [] - await node.endpoints[primary_value.endpoint].async_invoke_cc_api( + await node.endpoints[endpoint].async_invoke_cc_api( CommandClass.METER, "reset", *args, wait_for_result=False ) LOGGER.debug( @@ -385,11 +388,12 @@ class ZWaveListSensor(ZwaveSensorBase): config_entry, driver, info, entity_description, unit_of_measurement ) + property_key_name = self.info.primary_value.property_key_name # Entity class attributes self._attr_name = self.generate_name( include_value_name=True, alternate_value_name=self.info.primary_value.property_name, - additional_info=[self.info.primary_value.property_key_name], + additional_info=[property_key_name] if property_key_name else None, ) @property @@ -409,8 +413,10 @@ class ZWaveListSensor(ZwaveSensorBase): @property def extra_state_attributes(self) -> dict[str, str] | None: """Return the device specific state attributes.""" + if (value := self.info.primary_value.value) is None: + return None # add the value's int value as property for multi-value (list) items - return {ATTR_VALUE: self.info.primary_value.value} + return {ATTR_VALUE: value} class ZWaveConfigParameterSensor(ZwaveSensorBase): @@ -430,11 +436,12 @@ class ZWaveConfigParameterSensor(ZwaveSensorBase): ) self._primary_value = cast(ConfigurationValue, self.info.primary_value) + property_key_name = self.info.primary_value.property_key_name # Entity class attributes self._attr_name = self.generate_name( include_value_name=True, alternate_value_name=self.info.primary_value.property_name, - additional_info=[self.info.primary_value.property_key_name], + additional_info=[property_key_name] if property_key_name else None, name_suffix="Config Parameter", ) @@ -458,10 +465,13 @@ class ZWaveConfigParameterSensor(ZwaveSensorBase): @property def extra_state_attributes(self) -> dict[str, str] | None: """Return the device specific state attributes.""" - if self._primary_value.configuration_value_type == ConfigurationValueType.RANGE: + if ( + self._primary_value.configuration_value_type == ConfigurationValueType.RANGE + or (value := self.info.primary_value.value) is None + ): return None # add the value's int value as property for multi-value (list) items - return {ATTR_VALUE: self.info.primary_value.value} + return {ATTR_VALUE: value} class ZWaveNodeStatusSensor(SensorEntity):