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:
parent
927b59fe5a
commit
bd8c88f51b
18 changed files with 191 additions and 69 deletions
|
@ -77,7 +77,7 @@ class ZwaveBooleanNodeButton(ZWaveBaseEntity, ButtonEntity):
|
|||
|
||||
async def async_press(self) -> None:
|
||||
"""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):
|
||||
|
@ -100,6 +100,9 @@ class ZWaveNodePingButton(ButtonEntity):
|
|||
|
||||
async def async_poll_value(self, _: bool) -> None:
|
||||
"""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(
|
||||
"There is no value to refresh for this entity so the zwave_js.refresh_value"
|
||||
" service won't work for it"
|
||||
|
|
|
@ -437,7 +437,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
|
|||
except StopIteration:
|
||||
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:
|
||||
"""Set new target temperature."""
|
||||
|
@ -451,7 +451,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
|
|||
)
|
||||
target_temp: float | None = kwargs.get(ATTR_TEMPERATURE)
|
||||
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:
|
||||
setpoint_low: ZwaveValue = self._setpoint_value_or_raise(
|
||||
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_high: float | None = kwargs.get(ATTR_TARGET_TEMP_HIGH)
|
||||
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:
|
||||
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:
|
||||
"""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
|
||||
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:
|
||||
"""Set new target preset mode."""
|
||||
|
@ -487,7 +487,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
|
|||
preset_mode_value = self._hvac_presets.get(preset_mode)
|
||||
if preset_mode_value is None:
|
||||
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):
|
||||
|
|
|
@ -163,7 +163,7 @@ class CoverPositionMixin(ZWaveBaseEntity, CoverEntity):
|
|||
async def async_set_cover_position(self, **kwargs: Any) -> None:
|
||||
"""Move the cover to a specific position."""
|
||||
assert self._target_position_value
|
||||
await self.info.node.async_set_value(
|
||||
await self._async_set_value(
|
||||
self._target_position_value,
|
||||
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:
|
||||
"""Open the cover."""
|
||||
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
|
||||
)
|
||||
|
||||
async def async_close_cover(self, **kwargs: Any) -> None:
|
||||
"""Close cover."""
|
||||
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
|
||||
)
|
||||
|
||||
|
@ -186,7 +186,7 @@ class CoverPositionMixin(ZWaveBaseEntity, CoverEntity):
|
|||
"""Stop cover."""
|
||||
assert self._stop_position_value
|
||||
# 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):
|
||||
|
@ -259,7 +259,7 @@ class CoverTiltMixin(ZWaveBaseEntity, CoverEntity):
|
|||
async def async_set_cover_tilt_position(self, **kwargs: Any) -> None:
|
||||
"""Move the cover tilt to a specific position."""
|
||||
assert self._target_tilt_value
|
||||
await self.info.node.async_set_value(
|
||||
await self._async_set_value(
|
||||
self._target_tilt_value,
|
||||
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:
|
||||
"""Open the cover tilt."""
|
||||
assert self._target_tilt_value
|
||||
await self.info.node.async_set_value(
|
||||
self._target_tilt_value, self._fully_open_tilt
|
||||
)
|
||||
await self._async_set_value(self._target_tilt_value, self._fully_open_tilt)
|
||||
|
||||
async def async_close_cover_tilt(self, **kwargs: Any) -> None:
|
||||
"""Close the cover tilt."""
|
||||
assert self._target_tilt_value
|
||||
await self.info.node.async_set_value(
|
||||
self._target_tilt_value, self._fully_closed_tilt
|
||||
)
|
||||
await self._async_set_value(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)
|
||||
await self._async_set_value(self._stop_tilt_value, False)
|
||||
|
||||
|
||||
class ZWaveMultilevelSwitchCover(CoverPositionMixin):
|
||||
|
@ -455,8 +451,8 @@ class ZwaveMotorizedBarrier(ZWaveBaseEntity, CoverEntity):
|
|||
|
||||
async def async_open_cover(self, **kwargs: Any) -> None:
|
||||
"""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:
|
||||
"""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)
|
||||
|
|
|
@ -67,12 +67,20 @@ class ZWaveBaseEntity(Entity):
|
|||
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:
|
||||
"""Poll a value."""
|
||||
if not refresh_all_values:
|
||||
self.hass.async_create_task(
|
||||
self.info.node.async_poll_value(self.info.primary_value)
|
||||
)
|
||||
self.hass.async_create_task(self._async_poll_value(self.info.primary_value))
|
||||
LOGGER.info(
|
||||
(
|
||||
"Refreshing primary value %s for %s, "
|
||||
|
@ -84,7 +92,7 @@ class ZWaveBaseEntity(Entity):
|
|||
return
|
||||
|
||||
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(
|
||||
(
|
||||
|
|
|
@ -100,7 +100,7 @@ class ZwaveFan(ZWaveBaseEntity, FanEntity):
|
|||
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(
|
||||
self,
|
||||
|
@ -122,15 +122,13 @@ class ZwaveFan(ZWaveBaseEntity, FanEntity):
|
|||
# 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
|
||||
# UX.
|
||||
await self.info.node.async_set_value(
|
||||
self._target_value, SET_TO_PREVIOUS_VALUE
|
||||
)
|
||||
await self._async_set_value(self._target_value, SET_TO_PREVIOUS_VALUE)
|
||||
self._use_optimistic_state = True
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""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
|
||||
def is_on(self) -> bool | None:
|
||||
|
@ -174,13 +172,13 @@ class ValueMappingZwaveFan(ZwaveFan):
|
|||
async def async_set_percentage(self, percentage: int) -> None:
|
||||
"""Set the speed percentage of the fan."""
|
||||
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:
|
||||
"""Set new preset mode."""
|
||||
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._async_set_value(self._target_value, zwave_value)
|
||||
return
|
||||
|
||||
raise NotValidPresetModeError(
|
||||
|
@ -342,13 +340,13 @@ class ZwaveThermostatFan(ZWaveBaseEntity, FanEntity):
|
|||
"""Turn the device on."""
|
||||
if not self._fan_off:
|
||||
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:
|
||||
"""Turn the device off."""
|
||||
if not self._fan_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
|
||||
def is_on(self) -> bool | None:
|
||||
|
@ -377,7 +375,7 @@ class ZwaveThermostatFan(ZWaveBaseEntity, FanEntity):
|
|||
except StopIteration:
|
||||
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
|
||||
def preset_modes(self) -> list[str] | None:
|
||||
|
|
|
@ -175,7 +175,7 @@ class ZWaveHumidifier(ZWaveBaseEntity, HumidifierEntity):
|
|||
else:
|
||||
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:
|
||||
"""Turn off device."""
|
||||
|
@ -192,7 +192,7 @@ class ZWaveHumidifier(ZWaveBaseEntity, HumidifierEntity):
|
|||
else:
|
||||
return
|
||||
|
||||
await self.info.node.async_set_value(self._current_mode, new_mode)
|
||||
await self._async_set_value(self._current_mode, new_mode)
|
||||
|
||||
@property
|
||||
def target_humidity(self) -> int | None:
|
||||
|
@ -204,7 +204,7 @@ class ZWaveHumidifier(ZWaveBaseEntity, HumidifierEntity):
|
|||
async def async_set_humidity(self, humidity: int) -> None:
|
||||
"""Set new target humidity."""
|
||||
if self._setpoint:
|
||||
await self.info.node.async_set_value(self._setpoint, humidity)
|
||||
await self._async_set_value(self._setpoint, humidity)
|
||||
|
||||
@property
|
||||
def min_humidity(self) -> int:
|
||||
|
|
|
@ -324,9 +324,7 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
|
|||
color_name = MULTI_COLOR_MAP[color]
|
||||
colors_dict[color_name] = value
|
||||
# set updated color object
|
||||
await self.info.node.async_set_value(
|
||||
combined_color_val, colors_dict, zwave_transition
|
||||
)
|
||||
await self._async_set_value(combined_color_val, colors_dict, zwave_transition)
|
||||
|
||||
async def _async_set_brightness(
|
||||
self, brightness: int | None, transition: float | None = None
|
||||
|
@ -350,7 +348,7 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
|
|||
zwave_transition = {TRANSITION_DURATION_OPTION: "default"}
|
||||
|
||||
# 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
|
||||
)
|
||||
# We do an optimistic state update when setting to a previous value
|
||||
|
|
|
@ -13,12 +13,14 @@ from zwave_js_server.const.command_class.lock import (
|
|||
LOCK_CMD_CLASS_TO_PROPERTY_MAP,
|
||||
DoorLockMode,
|
||||
)
|
||||
from zwave_js_server.exceptions import BaseZwaveJSServerError
|
||||
from zwave_js_server.util.lock import clear_usercode, set_usercode
|
||||
|
||||
from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN, LockEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
@ -114,7 +116,7 @@ class ZWaveLock(ZWaveBaseEntity, LockEntity):
|
|||
]
|
||||
)
|
||||
if target_value is not None:
|
||||
await self.info.node.async_set_value(
|
||||
await self._async_set_value(
|
||||
target_value,
|
||||
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:
|
||||
"""Set the usercode to index X on the lock."""
|
||||
await set_usercode(self.info.node, code_slot, usercode)
|
||||
try:
|
||||
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)
|
||||
|
||||
async def async_clear_lock_usercode(self, code_slot: int) -> None:
|
||||
"""Clear the usercode at index X on the lock."""
|
||||
await clear_usercode(self.info.node, code_slot)
|
||||
try:
|
||||
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)
|
||||
|
|
|
@ -164,6 +164,6 @@ class ZwaveVolumeNumberEntity(ZWaveBaseEntity, NumberEntity):
|
|||
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Set new value."""
|
||||
await self.info.node.async_set_value(
|
||||
await self._async_set_value(
|
||||
self.info.primary_value, round(value * self.correction_factor)
|
||||
)
|
||||
|
|
|
@ -92,7 +92,7 @@ class ZwaveSelectEntity(ZWaveBaseEntity, SelectEntity):
|
|||
for key, val in self.info.primary_value.metadata.states.items()
|
||||
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):
|
||||
|
@ -162,7 +162,7 @@ class ZwaveDefaultToneSelectEntity(ZWaveBaseEntity, SelectEntity):
|
|||
for key, val in self._tones_value.metadata.states.items()
|
||||
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):
|
||||
|
@ -197,4 +197,4 @@ class ZwaveMultilevelSwitchSelectEntity(ZWaveBaseEntity, SelectEntity):
|
|||
"""Change the selected option."""
|
||||
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(self._target_value, int(key))
|
||||
await self._async_set_value(self._target_value, int(key))
|
||||
|
|
|
@ -11,6 +11,7 @@ from zwave_js_server.const.command_class.meter import (
|
|||
RESET_METER_OPTION_TARGET_VALUE,
|
||||
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.statistics import ControllerStatisticsDataType
|
||||
from zwave_js_server.model.driver import Driver
|
||||
|
@ -43,6 +44,7 @@ from homeassistant.const import (
|
|||
UnitOfTime,
|
||||
)
|
||||
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_platform import AddEntitiesCallback
|
||||
|
@ -671,9 +673,15 @@ class ZWaveMeterSensor(ZWaveNumericSensor):
|
|||
if value is not None:
|
||||
options[RESET_METER_OPTION_TARGET_VALUE] = value
|
||||
args = [options] if options else []
|
||||
await node.endpoints[endpoint].async_invoke_cc_api(
|
||||
CommandClass.METER, "reset", *args, wait_for_result=False
|
||||
)
|
||||
try:
|
||||
await node.endpoints[endpoint].async_invoke_cc_api(
|
||||
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(
|
||||
"Meters on node %s endpoint %s reset with the following options: %s",
|
||||
node,
|
||||
|
@ -802,6 +810,9 @@ class ZWaveNodeStatusSensor(SensorEntity):
|
|||
|
||||
async def async_poll_value(self, _: bool) -> None:
|
||||
"""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(
|
||||
"There is no value to refresh for this entity so the zwave_js.refresh_value"
|
||||
" service won't work for it"
|
||||
|
@ -878,7 +889,10 @@ class ZWaveStatisticsSensor(SensorEntity):
|
|||
|
||||
async def async_poll_value(self, _: bool) -> None:
|
||||
"""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"
|
||||
" service won't work for it"
|
||||
)
|
||||
|
|
|
@ -633,8 +633,11 @@ class ZWaveServices:
|
|||
"calls will still work for now but the service will be removed in a "
|
||||
"future release"
|
||||
)
|
||||
nodes: set[ZwaveNode] = service.data[const.ATTR_NODES]
|
||||
await asyncio.gather(*(node.async_ping() for node in nodes))
|
||||
nodes: list[ZwaveNode] = list(service.data[const.ATTR_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:
|
||||
"""Invoke a command class API."""
|
||||
|
|
|
@ -79,14 +79,6 @@ class ZwaveSirenEntity(ZWaveBaseEntity, SirenEntity):
|
|||
return None
|
||||
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:
|
||||
"""Turn the device on."""
|
||||
tone_id: int | None = kwargs.get(ATTR_TONE)
|
||||
|
@ -95,11 +87,13 @@ class ZwaveSirenEntity(ZWaveBaseEntity, SirenEntity):
|
|||
options["volume"] = round(volume * 100)
|
||||
# Play the default tone if a tone isn't provided
|
||||
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
|
||||
|
||||
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:
|
||||
"""Turn the device off."""
|
||||
await self.async_set_value(ToneID.OFF)
|
||||
await self._async_set_value(self.info.primary_value, ToneID.OFF)
|
||||
|
|
|
@ -291,6 +291,9 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity):
|
|||
|
||||
async def async_poll_value(self, _: bool) -> None:
|
||||
"""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(
|
||||
"There is no value to refresh for this entity so the zwave_js.refresh_value"
|
||||
" service won't work for it"
|
||||
|
|
|
@ -5,6 +5,7 @@ from zwave_js_server.const.command_class.thermostat import (
|
|||
THERMOSTAT_OPERATING_STATE_PROPERTY,
|
||||
)
|
||||
from zwave_js_server.event import Event
|
||||
from zwave_js_server.exceptions import FailedZWaveCommand
|
||||
from zwave_js_server.model.node import Node
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
|
@ -30,6 +31,7 @@ from homeassistant.components.climate import (
|
|||
HVACMode,
|
||||
)
|
||||
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.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
|
@ -49,7 +51,11 @@ from .common import (
|
|||
|
||||
|
||||
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:
|
||||
"""Test a thermostat v2 command class entity."""
|
||||
node = climate_radio_thermostat_ct100_plus
|
||||
|
@ -280,6 +286,20 @@ async def test_thermostat_v2(
|
|||
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(
|
||||
hass: HomeAssistant,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
"""Test the Z-Wave JS lock platform."""
|
||||
import pytest
|
||||
from zwave_js_server.const import CommandClass
|
||||
from zwave_js_server.const.command_class.lock import (
|
||||
ATTR_CODE_SLOT,
|
||||
|
@ -6,6 +7,7 @@ from zwave_js_server.const.command_class.lock import (
|
|||
CURRENT_MODE_PROPERTY,
|
||||
)
|
||||
from zwave_js_server.event import Event
|
||||
from zwave_js_server.exceptions import FailedZWaveCommand
|
||||
from zwave_js_server.model.node import Node, NodeStatus
|
||||
|
||||
from homeassistant.components.lock import (
|
||||
|
@ -27,6 +29,7 @@ from homeassistant.const import (
|
|||
STATE_UNLOCKED,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
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
|
||||
|
||||
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(
|
||||
type="dead",
|
||||
data={
|
||||
|
|
|
@ -4,6 +4,7 @@ import copy
|
|||
import pytest
|
||||
from zwave_js_server.const.command_class.meter import MeterType
|
||||
from zwave_js_server.event import Event
|
||||
from zwave_js_server.exceptions import FailedZWaveCommand
|
||||
from zwave_js_server.model.node import Node
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
|
@ -39,6 +40,7 @@ from homeassistant.const import (
|
|||
UnitOfTime,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
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.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(
|
||||
hass: HomeAssistant,
|
||||
|
@ -609,7 +623,7 @@ NODE_STATISTICS_SUFFIXES_UNKNOWN = {
|
|||
|
||||
|
||||
async def test_statistics_sensors(
|
||||
hass: HomeAssistant, zp3111, client, integration
|
||||
hass: HomeAssistant, zp3111, client, integration, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""Test statistics sensors."""
|
||||
ent_reg = er.async_get(hass)
|
||||
|
@ -730,10 +744,27 @@ async def test_statistics_sensors(
|
|||
(NODE_STATISTICS_ENTITY_PREFIX, NODE_STATISTICS_SUFFIXES_UNKNOWN),
|
||||
):
|
||||
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.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_power": {
|
||||
|
|
|
@ -1539,6 +1539,18 @@ async def test_ping(
|
|||
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(
|
||||
hass: HomeAssistant,
|
||||
|
|
Loading…
Add table
Reference in a new issue