Add error handling for all zwave_js service calls (#93846)

* Add error handling for all service calls

* Switch siren to use internal function

* Remove failing checks

* Revert change to poll service, add comments, and add additional error handling

* Add error handling for ping and refresh + review comment + add tests

* Add test for statistics entity refresh
This commit is contained in:
Raman Gupta 2023-05-31 11:09:01 -04:00 committed by GitHub
parent 927b59fe5a
commit bd8c88f51b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 191 additions and 69 deletions

View File

@ -77,7 +77,7 @@ class ZwaveBooleanNodeButton(ZWaveBaseEntity, ButtonEntity):
async def async_press(self) -> None: async def async_press(self) -> None:
"""Press the button.""" """Press the button."""
await self.info.node.async_set_value(self.info.primary_value, True) await self._async_set_value(self.info.primary_value, True)
class ZWaveNodePingButton(ButtonEntity): class ZWaveNodePingButton(ButtonEntity):
@ -100,6 +100,9 @@ class ZWaveNodePingButton(ButtonEntity):
async def async_poll_value(self, _: bool) -> None: async def async_poll_value(self, _: bool) -> None:
"""Poll a value.""" """Poll a value."""
# We log an error instead of raising an exception because this service call occurs
# in a separate task since it is called via the dispatcher and we don't want to
# raise the exception in that separate task because it is confusing to the user.
LOGGER.error( LOGGER.error(
"There is no value to refresh for this entity so the zwave_js.refresh_value" "There is no value to refresh for this entity so the zwave_js.refresh_value"
" service won't work for it" " service won't work for it"

View File

@ -437,7 +437,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
except StopIteration: except StopIteration:
raise ValueError(f"Received an invalid fan mode: {fan_mode}") from None raise ValueError(f"Received an invalid fan mode: {fan_mode}") from None
await self.info.node.async_set_value(self._fan_mode, new_state) await self._async_set_value(self._fan_mode, new_state)
async def async_set_temperature(self, **kwargs: Any) -> None: async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature.""" """Set new target temperature."""
@ -451,7 +451,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
) )
target_temp: float | None = kwargs.get(ATTR_TEMPERATURE) target_temp: float | None = kwargs.get(ATTR_TEMPERATURE)
if target_temp is not None: if target_temp is not None:
await self.info.node.async_set_value(setpoint, target_temp) await self._async_set_value(setpoint, target_temp)
elif len(self._current_mode_setpoint_enums) == 2: elif len(self._current_mode_setpoint_enums) == 2:
setpoint_low: ZwaveValue = self._setpoint_value_or_raise( setpoint_low: ZwaveValue = self._setpoint_value_or_raise(
self._current_mode_setpoint_enums[0] self._current_mode_setpoint_enums[0]
@ -462,9 +462,9 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
target_temp_low: float | None = kwargs.get(ATTR_TARGET_TEMP_LOW) target_temp_low: float | None = kwargs.get(ATTR_TARGET_TEMP_LOW)
target_temp_high: float | None = kwargs.get(ATTR_TARGET_TEMP_HIGH) target_temp_high: float | None = kwargs.get(ATTR_TARGET_TEMP_HIGH)
if target_temp_low is not None: if target_temp_low is not None:
await self.info.node.async_set_value(setpoint_low, target_temp_low) await self._async_set_value(setpoint_low, target_temp_low)
if target_temp_high is not None: if target_temp_high is not None:
await self.info.node.async_set_value(setpoint_high, target_temp_high) await self._async_set_value(setpoint_high, target_temp_high)
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set new target hvac mode.""" """Set new target hvac mode."""
@ -475,7 +475,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
# Thermostat(valve) has no support for setting a mode, so we make it a no-op # Thermostat(valve) has no support for setting a mode, so we make it a no-op
return return
await self.info.node.async_set_value(self._current_mode, hvac_mode_id) await self._async_set_value(self._current_mode, hvac_mode_id)
async def async_set_preset_mode(self, preset_mode: str) -> None: async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new target preset mode.""" """Set new target preset mode."""
@ -487,7 +487,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
preset_mode_value = self._hvac_presets.get(preset_mode) preset_mode_value = self._hvac_presets.get(preset_mode)
if preset_mode_value is None: if preset_mode_value is None:
raise ValueError(f"Received an invalid preset mode: {preset_mode}") raise ValueError(f"Received an invalid preset mode: {preset_mode}")
await self.info.node.async_set_value(self._current_mode, preset_mode_value) await self._async_set_value(self._current_mode, preset_mode_value)
class DynamicCurrentTempClimate(ZWaveClimate): class DynamicCurrentTempClimate(ZWaveClimate):

View File

@ -163,7 +163,7 @@ class CoverPositionMixin(ZWaveBaseEntity, CoverEntity):
async def async_set_cover_position(self, **kwargs: Any) -> None: async def async_set_cover_position(self, **kwargs: Any) -> None:
"""Move the cover to a specific position.""" """Move the cover to a specific position."""
assert self._target_position_value assert self._target_position_value
await self.info.node.async_set_value( await self._async_set_value(
self._target_position_value, self._target_position_value,
self.percent_to_zwave_position(kwargs[ATTR_POSITION]), self.percent_to_zwave_position(kwargs[ATTR_POSITION]),
) )
@ -171,14 +171,14 @@ class CoverPositionMixin(ZWaveBaseEntity, CoverEntity):
async def async_open_cover(self, **kwargs: Any) -> None: async def async_open_cover(self, **kwargs: Any) -> None:
"""Open the cover.""" """Open the cover."""
assert self._target_position_value assert self._target_position_value
await self.info.node.async_set_value( await self._async_set_value(
self._target_position_value, self._fully_open_position self._target_position_value, self._fully_open_position
) )
async def async_close_cover(self, **kwargs: Any) -> None: async def async_close_cover(self, **kwargs: Any) -> None:
"""Close cover.""" """Close cover."""
assert self._target_position_value assert self._target_position_value
await self.info.node.async_set_value( await self._async_set_value(
self._target_position_value, self._fully_closed_position self._target_position_value, self._fully_closed_position
) )
@ -186,7 +186,7 @@ class CoverPositionMixin(ZWaveBaseEntity, CoverEntity):
"""Stop cover.""" """Stop cover."""
assert self._stop_position_value assert self._stop_position_value
# Stop the cover, will stop regardless of the actual direction of travel. # Stop the cover, will stop regardless of the actual direction of travel.
await self.info.node.async_set_value(self._stop_position_value, False) await self._async_set_value(self._stop_position_value, False)
class CoverTiltMixin(ZWaveBaseEntity, CoverEntity): class CoverTiltMixin(ZWaveBaseEntity, CoverEntity):
@ -259,7 +259,7 @@ class CoverTiltMixin(ZWaveBaseEntity, CoverEntity):
async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: async def async_set_cover_tilt_position(self, **kwargs: Any) -> None:
"""Move the cover tilt to a specific position.""" """Move the cover tilt to a specific position."""
assert self._target_tilt_value assert self._target_tilt_value
await self.info.node.async_set_value( await self._async_set_value(
self._target_tilt_value, self._target_tilt_value,
self.percent_to_zwave_tilt(kwargs[ATTR_TILT_POSITION]), self.percent_to_zwave_tilt(kwargs[ATTR_TILT_POSITION]),
) )
@ -267,22 +267,18 @@ class CoverTiltMixin(ZWaveBaseEntity, CoverEntity):
async def async_open_cover_tilt(self, **kwargs: Any) -> None: async def async_open_cover_tilt(self, **kwargs: Any) -> None:
"""Open the cover tilt.""" """Open the cover tilt."""
assert self._target_tilt_value assert self._target_tilt_value
await self.info.node.async_set_value( await self._async_set_value(self._target_tilt_value, self._fully_open_tilt)
self._target_tilt_value, self._fully_open_tilt
)
async def async_close_cover_tilt(self, **kwargs: Any) -> None: async def async_close_cover_tilt(self, **kwargs: Any) -> None:
"""Close the cover tilt.""" """Close the cover tilt."""
assert self._target_tilt_value assert self._target_tilt_value
await self.info.node.async_set_value( await self._async_set_value(self._target_tilt_value, self._fully_closed_tilt)
self._target_tilt_value, self._fully_closed_tilt
)
async def async_stop_cover_tilt(self, **kwargs: Any) -> None: async def async_stop_cover_tilt(self, **kwargs: Any) -> None:
"""Stop the cover tilt.""" """Stop the cover tilt."""
assert self._stop_tilt_value assert self._stop_tilt_value
# Stop the tilt, will stop regardless of the actual direction of travel. # Stop the tilt, will stop regardless of the actual direction of travel.
await self.info.node.async_set_value(self._stop_tilt_value, False) await self._async_set_value(self._stop_tilt_value, False)
class ZWaveMultilevelSwitchCover(CoverPositionMixin): class ZWaveMultilevelSwitchCover(CoverPositionMixin):
@ -455,8 +451,8 @@ class ZwaveMotorizedBarrier(ZWaveBaseEntity, CoverEntity):
async def async_open_cover(self, **kwargs: Any) -> None: async def async_open_cover(self, **kwargs: Any) -> None:
"""Open the garage door.""" """Open the garage door."""
await self.info.node.async_set_value(self._target_state, BarrierState.OPEN) await self._async_set_value(self._target_state, BarrierState.OPEN)
async def async_close_cover(self, **kwargs: Any) -> None: async def async_close_cover(self, **kwargs: Any) -> None:
"""Close the garage door.""" """Close the garage door."""
await self.info.node.async_set_value(self._target_state, BarrierState.CLOSED) await self._async_set_value(self._target_state, BarrierState.CLOSED)

View File

@ -67,12 +67,20 @@ class ZWaveBaseEntity(Entity):
To be overridden by platforms needing this event. To be overridden by platforms needing this event.
""" """
async def _async_poll_value(self, value_or_id: str | ZwaveValue) -> None:
"""Poll a value."""
# We log an error instead of raising an exception because this service call occurs
# in a separate task and we don't want to raise the exception in that separate task
# because it is confusing to the user.
try:
await self.info.node.async_poll_value(value_or_id)
except BaseZwaveJSServerError as err:
LOGGER.error("Error while refreshing value %s: %s", value_or_id, err)
async def async_poll_value(self, refresh_all_values: bool) -> None: async def async_poll_value(self, refresh_all_values: bool) -> None:
"""Poll a value.""" """Poll a value."""
if not refresh_all_values: if not refresh_all_values:
self.hass.async_create_task( self.hass.async_create_task(self._async_poll_value(self.info.primary_value))
self.info.node.async_poll_value(self.info.primary_value)
)
LOGGER.info( LOGGER.info(
( (
"Refreshing primary value %s for %s, " "Refreshing primary value %s for %s, "
@ -84,7 +92,7 @@ class ZWaveBaseEntity(Entity):
return return
for value_id in self.watched_value_ids: for value_id in self.watched_value_ids:
self.hass.async_create_task(self.info.node.async_poll_value(value_id)) self.hass.async_create_task(self._async_poll_value(value_id))
LOGGER.info( LOGGER.info(
( (

View File

@ -100,7 +100,7 @@ class ZwaveFan(ZWaveBaseEntity, FanEntity):
percentage_to_ranged_value(DEFAULT_SPEED_RANGE, percentage) percentage_to_ranged_value(DEFAULT_SPEED_RANGE, percentage)
) )
await self.info.node.async_set_value(self._target_value, zwave_speed) await self._async_set_value(self._target_value, zwave_speed)
async def async_turn_on( async def async_turn_on(
self, self,
@ -122,15 +122,13 @@ class ZwaveFan(ZWaveBaseEntity, FanEntity):
# when setting to a previous value to avoid waiting for the value to be # when setting to a previous value to avoid waiting for the value to be
# updated from the device which is typically delayed and causes a confusing # updated from the device which is typically delayed and causes a confusing
# UX. # UX.
await self.info.node.async_set_value( await self._async_set_value(self._target_value, SET_TO_PREVIOUS_VALUE)
self._target_value, SET_TO_PREVIOUS_VALUE
)
self._use_optimistic_state = True self._use_optimistic_state = True
self.async_write_ha_state() self.async_write_ha_state()
async def async_turn_off(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the device off.""" """Turn the device off."""
await self.info.node.async_set_value(self._target_value, 0) await self._async_set_value(self._target_value, 0)
@property @property
def is_on(self) -> bool | None: def is_on(self) -> bool | None:
@ -174,13 +172,13 @@ class ValueMappingZwaveFan(ZwaveFan):
async def async_set_percentage(self, percentage: int) -> None: async def async_set_percentage(self, percentage: int) -> None:
"""Set the speed percentage of the fan.""" """Set the speed percentage of the fan."""
zwave_speed = self.percentage_to_zwave_speed(percentage) zwave_speed = self.percentage_to_zwave_speed(percentage)
await self.info.node.async_set_value(self._target_value, zwave_speed) await self._async_set_value(self._target_value, zwave_speed)
async def async_set_preset_mode(self, preset_mode: str) -> None: async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode.""" """Set new preset mode."""
for zwave_value, mapped_preset_mode in self.fan_value_mapping.presets.items(): for zwave_value, mapped_preset_mode in self.fan_value_mapping.presets.items():
if preset_mode == mapped_preset_mode: if preset_mode == mapped_preset_mode:
await self.info.node.async_set_value(self._target_value, zwave_value) await self._async_set_value(self._target_value, zwave_value)
return return
raise NotValidPresetModeError( raise NotValidPresetModeError(
@ -342,13 +340,13 @@ class ZwaveThermostatFan(ZWaveBaseEntity, FanEntity):
"""Turn the device on.""" """Turn the device on."""
if not self._fan_off: if not self._fan_off:
raise HomeAssistantError("Unhandled action turn_on") raise HomeAssistantError("Unhandled action turn_on")
await self.info.node.async_set_value(self._fan_off, False) await self._async_set_value(self._fan_off, False)
async def async_turn_off(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the device off.""" """Turn the device off."""
if not self._fan_off: if not self._fan_off:
raise HomeAssistantError("Unhandled action turn_off") raise HomeAssistantError("Unhandled action turn_off")
await self.info.node.async_set_value(self._fan_off, True) await self._async_set_value(self._fan_off, True)
@property @property
def is_on(self) -> bool | None: def is_on(self) -> bool | None:
@ -377,7 +375,7 @@ class ZwaveThermostatFan(ZWaveBaseEntity, FanEntity):
except StopIteration: except StopIteration:
raise ValueError(f"Received an invalid fan mode: {preset_mode}") from None raise ValueError(f"Received an invalid fan mode: {preset_mode}") from None
await self.info.node.async_set_value(self._fan_mode, new_state) await self._async_set_value(self._fan_mode, new_state)
@property @property
def preset_modes(self) -> list[str] | None: def preset_modes(self) -> list[str] | None:

View File

@ -175,7 +175,7 @@ class ZWaveHumidifier(ZWaveBaseEntity, HumidifierEntity):
else: else:
return return
await self.info.node.async_set_value(self._current_mode, new_mode) await self._async_set_value(self._current_mode, new_mode)
async def async_turn_off(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off device.""" """Turn off device."""
@ -192,7 +192,7 @@ class ZWaveHumidifier(ZWaveBaseEntity, HumidifierEntity):
else: else:
return return
await self.info.node.async_set_value(self._current_mode, new_mode) await self._async_set_value(self._current_mode, new_mode)
@property @property
def target_humidity(self) -> int | None: def target_humidity(self) -> int | None:
@ -204,7 +204,7 @@ class ZWaveHumidifier(ZWaveBaseEntity, HumidifierEntity):
async def async_set_humidity(self, humidity: int) -> None: async def async_set_humidity(self, humidity: int) -> None:
"""Set new target humidity.""" """Set new target humidity."""
if self._setpoint: if self._setpoint:
await self.info.node.async_set_value(self._setpoint, humidity) await self._async_set_value(self._setpoint, humidity)
@property @property
def min_humidity(self) -> int: def min_humidity(self) -> int:

View File

@ -324,9 +324,7 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
color_name = MULTI_COLOR_MAP[color] color_name = MULTI_COLOR_MAP[color]
colors_dict[color_name] = value colors_dict[color_name] = value
# set updated color object # set updated color object
await self.info.node.async_set_value( await self._async_set_value(combined_color_val, colors_dict, zwave_transition)
combined_color_val, colors_dict, zwave_transition
)
async def _async_set_brightness( async def _async_set_brightness(
self, brightness: int | None, transition: float | None = None self, brightness: int | None, transition: float | None = None
@ -350,7 +348,7 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
zwave_transition = {TRANSITION_DURATION_OPTION: "default"} zwave_transition = {TRANSITION_DURATION_OPTION: "default"}
# setting a value requires setting targetValue # setting a value requires setting targetValue
await self.info.node.async_set_value( await self._async_set_value(
self._target_brightness, zwave_brightness, zwave_transition self._target_brightness, zwave_brightness, zwave_transition
) )
# We do an optimistic state update when setting to a previous value # We do an optimistic state update when setting to a previous value

View File

@ -13,12 +13,14 @@ from zwave_js_server.const.command_class.lock import (
LOCK_CMD_CLASS_TO_PROPERTY_MAP, LOCK_CMD_CLASS_TO_PROPERTY_MAP,
DoorLockMode, DoorLockMode,
) )
from zwave_js_server.exceptions import BaseZwaveJSServerError
from zwave_js_server.util.lock import clear_usercode, set_usercode from zwave_js_server.util.lock import clear_usercode, set_usercode
from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN, LockEntity from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN, LockEntity
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
@ -114,7 +116,7 @@ class ZWaveLock(ZWaveBaseEntity, LockEntity):
] ]
) )
if target_value is not None: if target_value is not None:
await self.info.node.async_set_value( await self._async_set_value(
target_value, target_value,
STATE_TO_ZWAVE_MAP[self.info.primary_value.command_class][target_state], STATE_TO_ZWAVE_MAP[self.info.primary_value.command_class][target_state],
) )
@ -129,10 +131,20 @@ class ZWaveLock(ZWaveBaseEntity, LockEntity):
async def async_set_lock_usercode(self, code_slot: int, usercode: str) -> None: async def async_set_lock_usercode(self, code_slot: int, usercode: str) -> None:
"""Set the usercode to index X on the lock.""" """Set the usercode to index X on the lock."""
try:
await set_usercode(self.info.node, code_slot, usercode) await set_usercode(self.info.node, code_slot, usercode)
except BaseZwaveJSServerError as err:
raise HomeAssistantError(
f"Unable to set lock usercode on code_slot {code_slot}: {err}"
) from err
LOGGER.debug("User code at slot %s set", code_slot) LOGGER.debug("User code at slot %s set", code_slot)
async def async_clear_lock_usercode(self, code_slot: int) -> None: async def async_clear_lock_usercode(self, code_slot: int) -> None:
"""Clear the usercode at index X on the lock.""" """Clear the usercode at index X on the lock."""
try:
await clear_usercode(self.info.node, code_slot) await clear_usercode(self.info.node, code_slot)
except BaseZwaveJSServerError as err:
raise HomeAssistantError(
f"Unable to clear lock usercode on code_slot {code_slot}: {err}"
) from err
LOGGER.debug("User code at slot %s cleared", code_slot) LOGGER.debug("User code at slot %s cleared", code_slot)

View File

@ -164,6 +164,6 @@ class ZwaveVolumeNumberEntity(ZWaveBaseEntity, NumberEntity):
async def async_set_native_value(self, value: float) -> None: async def async_set_native_value(self, value: float) -> None:
"""Set new value.""" """Set new value."""
await self.info.node.async_set_value( await self._async_set_value(
self.info.primary_value, round(value * self.correction_factor) self.info.primary_value, round(value * self.correction_factor)
) )

View File

@ -92,7 +92,7 @@ class ZwaveSelectEntity(ZWaveBaseEntity, SelectEntity):
for key, val in self.info.primary_value.metadata.states.items() for key, val in self.info.primary_value.metadata.states.items()
if val == option if val == option
) )
await self.info.node.async_set_value(self.info.primary_value, int(key)) await self._async_set_value(self.info.primary_value, int(key))
class ZWaveConfigParameterSelectEntity(ZwaveSelectEntity): class ZWaveConfigParameterSelectEntity(ZwaveSelectEntity):
@ -162,7 +162,7 @@ class ZwaveDefaultToneSelectEntity(ZWaveBaseEntity, SelectEntity):
for key, val in self._tones_value.metadata.states.items() for key, val in self._tones_value.metadata.states.items()
if val == option if val == option
) )
await self.info.node.async_set_value(self.info.primary_value, int(key)) await self._async_set_value(self.info.primary_value, int(key))
class ZwaveMultilevelSwitchSelectEntity(ZWaveBaseEntity, SelectEntity): class ZwaveMultilevelSwitchSelectEntity(ZWaveBaseEntity, SelectEntity):
@ -197,4 +197,4 @@ class ZwaveMultilevelSwitchSelectEntity(ZWaveBaseEntity, SelectEntity):
"""Change the selected option.""" """Change the selected option."""
assert self._target_value is not None assert self._target_value is not None
key = next(key for key, val in self._lookup_map.items() if val == option) 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._async_set_value(self._target_value, int(key))

View File

@ -11,6 +11,7 @@ from zwave_js_server.const.command_class.meter import (
RESET_METER_OPTION_TARGET_VALUE, RESET_METER_OPTION_TARGET_VALUE,
RESET_METER_OPTION_TYPE, RESET_METER_OPTION_TYPE,
) )
from zwave_js_server.exceptions import BaseZwaveJSServerError
from zwave_js_server.model.controller import Controller from zwave_js_server.model.controller import Controller
from zwave_js_server.model.controller.statistics import ControllerStatisticsDataType from zwave_js_server.model.controller.statistics import ControllerStatisticsDataType
from zwave_js_server.model.driver import Driver from zwave_js_server.model.driver import Driver
@ -43,6 +44,7 @@ from homeassistant.const import (
UnitOfTime, UnitOfTime,
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_platform from homeassistant.helpers import entity_platform
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
@ -671,9 +673,15 @@ class ZWaveMeterSensor(ZWaveNumericSensor):
if value is not None: if value is not None:
options[RESET_METER_OPTION_TARGET_VALUE] = value options[RESET_METER_OPTION_TARGET_VALUE] = value
args = [options] if options else [] args = [options] if options else []
try:
await node.endpoints[endpoint].async_invoke_cc_api( await node.endpoints[endpoint].async_invoke_cc_api(
CommandClass.METER, "reset", *args, wait_for_result=False CommandClass.METER, "reset", *args, wait_for_result=False
) )
except BaseZwaveJSServerError as err:
LOGGER.error(
"Failed to reset meters on node %s endpoint %s: %s", node, endpoint, err
)
raise HomeAssistantError from err
LOGGER.debug( LOGGER.debug(
"Meters on node %s endpoint %s reset with the following options: %s", "Meters on node %s endpoint %s reset with the following options: %s",
node, node,
@ -802,6 +810,9 @@ class ZWaveNodeStatusSensor(SensorEntity):
async def async_poll_value(self, _: bool) -> None: async def async_poll_value(self, _: bool) -> None:
"""Poll a value.""" """Poll a value."""
# We log an error instead of raising an exception because this service call occurs
# in a separate task since it is called via the dispatcher and we don't want to
# raise the exception in that separate task because it is confusing to the user.
LOGGER.error( LOGGER.error(
"There is no value to refresh for this entity so the zwave_js.refresh_value" "There is no value to refresh for this entity so the zwave_js.refresh_value"
" service won't work for it" " service won't work for it"
@ -878,7 +889,10 @@ class ZWaveStatisticsSensor(SensorEntity):
async def async_poll_value(self, _: bool) -> None: async def async_poll_value(self, _: bool) -> None:
"""Poll a value.""" """Poll a value."""
raise ValueError( # We log an error instead of raising an exception because this service call occurs
# in a separate task since it is called via the dispatcher and we don't want to
# raise the exception in that separate task because it is confusing to the user.
LOGGER.error(
"There is no value to refresh for this entity so the zwave_js.refresh_value" "There is no value to refresh for this entity so the zwave_js.refresh_value"
" service won't work for it" " service won't work for it"
) )

View File

@ -633,8 +633,11 @@ class ZWaveServices:
"calls will still work for now but the service will be removed in a " "calls will still work for now but the service will be removed in a "
"future release" "future release"
) )
nodes: set[ZwaveNode] = service.data[const.ATTR_NODES] nodes: list[ZwaveNode] = list(service.data[const.ATTR_NODES])
await asyncio.gather(*(node.async_ping() for node in nodes)) results = await asyncio.gather(
*(node.async_ping() for node in nodes), return_exceptions=True
)
raise_exceptions_from_results(nodes, results)
async def async_invoke_cc_api(self, service: ServiceCall) -> None: async def async_invoke_cc_api(self, service: ServiceCall) -> None:
"""Invoke a command class API.""" """Invoke a command class API."""

View File

@ -79,14 +79,6 @@ class ZwaveSirenEntity(ZWaveBaseEntity, SirenEntity):
return None return None
return bool(self.info.primary_value.value) return bool(self.info.primary_value.value)
async def async_set_value(
self, new_value: int, options: dict[str, Any] | None = None
) -> None:
"""Set a value on a siren node."""
await self.info.node.async_set_value(
self.info.primary_value, new_value, options=options
)
async def async_turn_on(self, **kwargs: Any) -> None: async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the device on.""" """Turn the device on."""
tone_id: int | None = kwargs.get(ATTR_TONE) tone_id: int | None = kwargs.get(ATTR_TONE)
@ -95,11 +87,13 @@ class ZwaveSirenEntity(ZWaveBaseEntity, SirenEntity):
options["volume"] = round(volume * 100) options["volume"] = round(volume * 100)
# Play the default tone if a tone isn't provided # Play the default tone if a tone isn't provided
if tone_id is None: if tone_id is None:
await self.async_set_value(ToneID.DEFAULT, options) await self._async_set_value(
self.info.primary_value, ToneID.DEFAULT, options
)
return return
await self.async_set_value(tone_id, options) await self._async_set_value(self.info.primary_value, tone_id, options)
async def async_turn_off(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the device off.""" """Turn the device off."""
await self.async_set_value(ToneID.OFF) await self._async_set_value(self.info.primary_value, ToneID.OFF)

View File

@ -291,6 +291,9 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity):
async def async_poll_value(self, _: bool) -> None: async def async_poll_value(self, _: bool) -> None:
"""Poll a value.""" """Poll a value."""
# We log an error instead of raising an exception because this service call occurs
# in a separate task since it is called via the dispatcher and we don't want to
# raise the exception in that separate task because it is confusing to the user.
LOGGER.error( LOGGER.error(
"There is no value to refresh for this entity so the zwave_js.refresh_value" "There is no value to refresh for this entity so the zwave_js.refresh_value"
" service won't work for it" " service won't work for it"

View File

@ -5,6 +5,7 @@ from zwave_js_server.const.command_class.thermostat import (
THERMOSTAT_OPERATING_STATE_PROPERTY, THERMOSTAT_OPERATING_STATE_PROPERTY,
) )
from zwave_js_server.event import Event from zwave_js_server.event import Event
from zwave_js_server.exceptions import FailedZWaveCommand
from zwave_js_server.model.node import Node from zwave_js_server.model.node import Node
from homeassistant.components.climate import ( from homeassistant.components.climate import (
@ -30,6 +31,7 @@ from homeassistant.components.climate import (
HVACMode, HVACMode,
) )
from homeassistant.components.zwave_js.climate import ATTR_FAN_STATE from homeassistant.components.zwave_js.climate import ATTR_FAN_STATE
from homeassistant.components.zwave_js.const import DOMAIN, SERVICE_REFRESH_VALUE
from homeassistant.components.zwave_js.helpers import ZwaveValueMatcher from homeassistant.components.zwave_js.helpers import ZwaveValueMatcher
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_ENTITY_ID,
@ -49,7 +51,11 @@ from .common import (
async def test_thermostat_v2( async def test_thermostat_v2(
hass: HomeAssistant, client, climate_radio_thermostat_ct100_plus, integration hass: HomeAssistant,
client,
climate_radio_thermostat_ct100_plus,
integration,
caplog: pytest.LogCaptureFixture,
) -> None: ) -> None:
"""Test a thermostat v2 command class entity.""" """Test a thermostat v2 command class entity."""
node = climate_radio_thermostat_ct100_plus node = climate_radio_thermostat_ct100_plus
@ -280,6 +286,20 @@ async def test_thermostat_v2(
blocking=True, blocking=True,
) )
# Refresh value should log an error when there is an issue
client.async_send_command.reset_mock()
client.async_send_command.side_effect = FailedZWaveCommand("test", 1, "test")
await hass.services.async_call(
DOMAIN,
SERVICE_REFRESH_VALUE,
{
ATTR_ENTITY_ID: CLIMATE_RADIO_THERMOSTAT_ENTITY,
},
blocking=True,
)
assert "Error while refreshing value" in caplog.text
async def test_thermostat_different_endpoints( async def test_thermostat_different_endpoints(
hass: HomeAssistant, hass: HomeAssistant,

View File

@ -1,4 +1,5 @@
"""Test the Z-Wave JS lock platform.""" """Test the Z-Wave JS lock platform."""
import pytest
from zwave_js_server.const import CommandClass from zwave_js_server.const import CommandClass
from zwave_js_server.const.command_class.lock import ( from zwave_js_server.const.command_class.lock import (
ATTR_CODE_SLOT, ATTR_CODE_SLOT,
@ -6,6 +7,7 @@ from zwave_js_server.const.command_class.lock import (
CURRENT_MODE_PROPERTY, CURRENT_MODE_PROPERTY,
) )
from zwave_js_server.event import Event from zwave_js_server.event import Event
from zwave_js_server.exceptions import FailedZWaveCommand
from zwave_js_server.model.node import Node, NodeStatus from zwave_js_server.model.node import Node, NodeStatus
from homeassistant.components.lock import ( from homeassistant.components.lock import (
@ -27,6 +29,7 @@ from homeassistant.const import (
STATE_UNLOCKED, STATE_UNLOCKED,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from .common import SCHLAGE_BE469_LOCK_ENTITY, replace_value_of_zwave_value from .common import SCHLAGE_BE469_LOCK_ENTITY, replace_value_of_zwave_value
@ -153,6 +156,33 @@ async def test_door_lock(
} }
assert args["value"] == 0 assert args["value"] == 0
client.async_send_command.reset_mock()
client.async_send_command.side_effect = FailedZWaveCommand("test", 1, "test")
# Test set usercode service error handling
with pytest.raises(HomeAssistantError):
await hass.services.async_call(
ZWAVE_JS_DOMAIN,
SERVICE_SET_LOCK_USERCODE,
{
ATTR_ENTITY_ID: SCHLAGE_BE469_LOCK_ENTITY,
ATTR_CODE_SLOT: 1,
ATTR_USERCODE: "1234",
},
blocking=True,
)
# Test clear usercode service error handling
with pytest.raises(HomeAssistantError):
await hass.services.async_call(
ZWAVE_JS_DOMAIN,
SERVICE_CLEAR_LOCK_USERCODE,
{ATTR_ENTITY_ID: SCHLAGE_BE469_LOCK_ENTITY, ATTR_CODE_SLOT: 1},
blocking=True,
)
client.async_send_command.reset_mock()
event = Event( event = Event(
type="dead", type="dead",
data={ data={

View File

@ -4,6 +4,7 @@ import copy
import pytest import pytest
from zwave_js_server.const.command_class.meter import MeterType from zwave_js_server.const.command_class.meter import MeterType
from zwave_js_server.event import Event from zwave_js_server.event import Event
from zwave_js_server.exceptions import FailedZWaveCommand
from zwave_js_server.model.node import Node from zwave_js_server.model.node import Node
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
@ -39,6 +40,7 @@ from homeassistant.const import (
UnitOfTime, UnitOfTime,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from .common import ( from .common import (
@ -420,6 +422,18 @@ async def test_reset_meter(
client.async_send_command_no_wait.reset_mock() client.async_send_command_no_wait.reset_mock()
client.async_send_command_no_wait.side_effect = FailedZWaveCommand(
"test", 1, "test"
)
with pytest.raises(HomeAssistantError):
await hass.services.async_call(
DOMAIN,
SERVICE_RESET_METER,
{ATTR_ENTITY_ID: METER_ENERGY_SENSOR},
blocking=True,
)
async def test_meter_attributes( async def test_meter_attributes(
hass: HomeAssistant, hass: HomeAssistant,
@ -609,7 +623,7 @@ NODE_STATISTICS_SUFFIXES_UNKNOWN = {
async def test_statistics_sensors( async def test_statistics_sensors(
hass: HomeAssistant, zp3111, client, integration hass: HomeAssistant, zp3111, client, integration, caplog: pytest.LogCaptureFixture
) -> None: ) -> None:
"""Test statistics sensors.""" """Test statistics sensors."""
ent_reg = er.async_get(hass) ent_reg = er.async_get(hass)
@ -730,10 +744,27 @@ async def test_statistics_sensors(
(NODE_STATISTICS_ENTITY_PREFIX, NODE_STATISTICS_SUFFIXES_UNKNOWN), (NODE_STATISTICS_ENTITY_PREFIX, NODE_STATISTICS_SUFFIXES_UNKNOWN),
): ):
for suffix_key, val in suffixes.items(): for suffix_key, val in suffixes.items():
state = hass.states.get(f"{prefix}{suffix_key}") entity_id = f"{prefix}{suffix_key}"
state = hass.states.get(entity_id)
assert state assert state
assert state.state == str(val) assert state.state == str(val)
await hass.services.async_call(
DOMAIN,
SERVICE_REFRESH_VALUE,
{ATTR_ENTITY_ID: entity_id},
blocking=True,
)
assert caplog.text.count("There is no value to refresh for this entity") == len(
[
*CONTROLLER_STATISTICS_SUFFIXES,
*CONTROLLER_STATISTICS_SUFFIXES_UNKNOWN,
*NODE_STATISTICS_SUFFIXES,
*NODE_STATISTICS_SUFFIXES_UNKNOWN,
]
)
ENERGY_PRODUCTION_ENTITY_MAP = { ENERGY_PRODUCTION_ENTITY_MAP = {
"energy_production_power": { "energy_production_power": {

View File

@ -1539,6 +1539,18 @@ async def test_ping(
blocking=True, blocking=True,
) )
client.async_send_command.reset_mock()
client.async_send_command.side_effect = FailedZWaveCommand("test", 1, "test")
with pytest.raises(HomeAssistantError):
await hass.services.async_call(
DOMAIN,
SERVICE_PING,
{
ATTR_ENTITY_ID: CLIMATE_RADIO_THERMOSTAT_ENTITY,
},
blocking=True,
)
async def test_invoke_cc_api( async def test_invoke_cc_api(
hass: HomeAssistant, hass: HomeAssistant,