mirror of
https://github.com/home-assistant/core.git
synced 2026-09-24 15:31:52 -05:00
Add pylint checker for swallowed exceptions in action handlers (#170652)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
02666f8762
commit
017f85243a
@@ -44,6 +44,7 @@ def _change_setting(call: ServiceCall) -> None:
|
||||
|
||||
try:
|
||||
_get_abode_system(call.hass).abode.set_setting(setting, value)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except AbodeException as ex:
|
||||
LOGGER.warning(ex)
|
||||
|
||||
|
||||
@@ -116,6 +116,7 @@ class AdGuardHomeSwitch(AdGuardHomeEntity, SwitchEntity):
|
||||
"""Turn off the switch."""
|
||||
try:
|
||||
await self.entity_description.turn_off_fn(self.adguard)()
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except AdGuardHomeError:
|
||||
LOGGER.error("An error occurred while turning off AdGuard Home switch")
|
||||
self._attr_available = False
|
||||
@@ -124,6 +125,7 @@ class AdGuardHomeSwitch(AdGuardHomeEntity, SwitchEntity):
|
||||
"""Turn on the switch."""
|
||||
try:
|
||||
await self.entity_description.turn_on_fn(self.adguard)()
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except AdGuardHomeError:
|
||||
LOGGER.error("An error occurred while turning on AdGuard Home switch")
|
||||
self._attr_available = False
|
||||
|
||||
@@ -83,6 +83,7 @@ class AirobotButton(AirobotEntity, ButtonEntity):
|
||||
"""Handle the button press."""
|
||||
try:
|
||||
await self.entity_description.press_fn(self.coordinator)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except AirobotConnectionError, AirobotTimeoutError:
|
||||
# Connection errors during reboot are expected as device restarts
|
||||
pass
|
||||
|
||||
@@ -96,6 +96,7 @@ class ArcamFmj(ArcamFmjEntity, MediaPlayerEntity):
|
||||
"""Select a specific source."""
|
||||
try:
|
||||
value = SourceCodes[source]
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except KeyError:
|
||||
_LOGGER.error("Unsupported source %s", source)
|
||||
return
|
||||
|
||||
@@ -108,6 +108,7 @@ class ServiceBusNotificationService(BaseNotificationService):
|
||||
)
|
||||
try:
|
||||
await self._client.send_messages(queue_message)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except ServiceBusError as err:
|
||||
_LOGGER.error(
|
||||
"Could not send service bus notification to %s. %s",
|
||||
|
||||
@@ -251,6 +251,7 @@ class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity):
|
||||
|
||||
try:
|
||||
await device.async_request(device.api.send_data, code)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except (BroadlinkException, OSError) as err:
|
||||
_LOGGER.error("Error during %s: %s", service, err)
|
||||
break
|
||||
@@ -301,6 +302,7 @@ class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity):
|
||||
if toggle:
|
||||
code = [code, await learn_command(command)]
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except (AuthorizationError, NetworkTimeoutError, OSError) as err:
|
||||
_LOGGER.error("Failed to learn '%s': %s", command, err)
|
||||
break
|
||||
|
||||
@@ -717,6 +717,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
|
||||
await self.hass.async_add_executor_job(
|
||||
self._quick_play, app_name, app_data
|
||||
)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except NotImplementedError:
|
||||
_LOGGER.error("App %s not supported", app_name)
|
||||
return
|
||||
|
||||
@@ -59,6 +59,7 @@ class CiscoWebexNotificationService(BaseNotificationService):
|
||||
|
||||
try:
|
||||
self.client.messages.create(roomId=self.room, html=f"{title}{message}")
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except ApiError as api_error:
|
||||
_LOGGER.error(
|
||||
"Could not send Cisco Webex notification. Error: %s", api_error
|
||||
|
||||
@@ -127,6 +127,7 @@ async def async_handle_service(service_call: ServiceCall) -> None:
|
||||
_extract_color_from_path, service_call.hass, image_reference
|
||||
)
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except UnidentifiedImageError as ex:
|
||||
_LOGGER.error(
|
||||
"Bad image from %s '%s' provided, are you sure it's an image? %s",
|
||||
|
||||
@@ -66,6 +66,7 @@ class CommandLineNotificationService(BaseNotificationService):
|
||||
proc.returncode,
|
||||
command,
|
||||
)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except subprocess.TimeoutExpired:
|
||||
_LOGGER.error("Timeout for command: %s", command)
|
||||
kill_subprocess(proc)
|
||||
|
||||
@@ -85,6 +85,7 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
||||
else:
|
||||
try:
|
||||
hub = get_master_hub(hass)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except ValueError:
|
||||
LOGGER.error("No master gateway available")
|
||||
return
|
||||
|
||||
@@ -161,6 +161,7 @@ class DecoraWifiLight(LightEntity):
|
||||
|
||||
try:
|
||||
self._switch.update_attributes(attribs)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except ValueError:
|
||||
_LOGGER.error("Failed to turn on myLeviton switch")
|
||||
|
||||
@@ -169,6 +170,7 @@ class DecoraWifiLight(LightEntity):
|
||||
attribs = {"power": "OFF"}
|
||||
try:
|
||||
self._switch.update_attributes(attribs)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except ValueError:
|
||||
_LOGGER.error("Failed to turn off myLeviton switch")
|
||||
|
||||
|
||||
@@ -390,66 +390,79 @@ class DenonDevice(MediaPlayerEntity):
|
||||
"""Status of DynamicEQ."""
|
||||
return self._receiver.dynamic_eq
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@async_log_errors
|
||||
async def async_media_play_pause(self) -> None:
|
||||
"""Play or pause the media player."""
|
||||
await self._receiver.async_toggle_play_pause()
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@async_log_errors
|
||||
async def async_media_play(self) -> None:
|
||||
"""Send play command."""
|
||||
await self._receiver.async_play()
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@async_log_errors
|
||||
async def async_media_pause(self) -> None:
|
||||
"""Send pause command."""
|
||||
await self._receiver.async_pause()
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@async_log_errors
|
||||
async def async_media_stop(self) -> None:
|
||||
"""Send stop command."""
|
||||
await self._receiver.async_stop()
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@async_log_errors
|
||||
async def async_media_previous_track(self) -> None:
|
||||
"""Send previous track command."""
|
||||
await self._receiver.async_previous_track()
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@async_log_errors
|
||||
async def async_media_next_track(self) -> None:
|
||||
"""Send next track command."""
|
||||
await self._receiver.async_next_track()
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@async_log_errors
|
||||
async def async_select_source(self, source: str) -> None:
|
||||
"""Select input source."""
|
||||
await self._receiver.async_set_input_func(source)
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@async_log_errors
|
||||
async def async_select_sound_mode(self, sound_mode: str) -> None:
|
||||
"""Select sound mode."""
|
||||
await self._receiver.async_set_sound_mode(sound_mode)
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@async_log_errors
|
||||
async def async_turn_on(self) -> None:
|
||||
"""Turn on media player."""
|
||||
await self._receiver.async_power_on()
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@async_log_errors
|
||||
async def async_turn_off(self) -> None:
|
||||
"""Turn off media player."""
|
||||
await self._receiver.async_power_off()
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@async_log_errors
|
||||
async def async_volume_up(self) -> None:
|
||||
"""Volume up the media player."""
|
||||
await self._receiver.async_volume_up()
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@async_log_errors
|
||||
async def async_volume_down(self) -> None:
|
||||
"""Volume down media player."""
|
||||
await self._receiver.async_volume_down()
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@async_log_errors
|
||||
async def async_set_volume_level(self, volume: float) -> None:
|
||||
"""Set volume level, range 0..1."""
|
||||
@@ -460,6 +473,7 @@ class DenonDevice(MediaPlayerEntity):
|
||||
volume_denon = float(18)
|
||||
await self._receiver.async_set_volume(volume_denon)
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@async_log_errors
|
||||
async def async_mute_volume(self, mute: bool) -> None:
|
||||
"""Send mute command."""
|
||||
|
||||
@@ -90,6 +90,7 @@ class DIRECTVRemote(DIRECTVEntity, RemoteEntity):
|
||||
for single_command in command:
|
||||
try:
|
||||
await self.dtv.remote(single_command, self._address)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except DIRECTVError:
|
||||
_LOGGER.exception(
|
||||
"Sending command %s to device %s failed",
|
||||
|
||||
@@ -193,6 +193,7 @@ class DiscordNotificationService(BaseNotificationService):
|
||||
_LOGGER.warning("Channel not found for ID: %s", channelid)
|
||||
continue
|
||||
await channel.send(message, files=files, embeds=embeds)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except (nextcord.HTTPException, nextcord.NotFound) as error:
|
||||
_LOGGER.warning("Communication error: %s", error)
|
||||
await discord_bot.close()
|
||||
|
||||
@@ -605,6 +605,7 @@ class DlnaDmrEntity(MediaPlayerEntity):
|
||||
return None
|
||||
return self._device.volume_level
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@catch_request_errors
|
||||
async def async_set_volume_level(self, volume: float) -> None:
|
||||
"""Set volume level, range 0..1."""
|
||||
@@ -618,6 +619,7 @@ class DlnaDmrEntity(MediaPlayerEntity):
|
||||
return None
|
||||
return self._device.is_volume_muted
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@catch_request_errors
|
||||
async def async_mute_volume(self, mute: bool) -> None:
|
||||
"""Mute the volume."""
|
||||
@@ -625,24 +627,28 @@ class DlnaDmrEntity(MediaPlayerEntity):
|
||||
desired_mute = bool(mute)
|
||||
await self._device.async_mute_volume(desired_mute)
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@catch_request_errors
|
||||
async def async_media_pause(self) -> None:
|
||||
"""Send pause command."""
|
||||
assert self._device is not None
|
||||
await self._device.async_pause()
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@catch_request_errors
|
||||
async def async_media_play(self) -> None:
|
||||
"""Send play command."""
|
||||
assert self._device is not None
|
||||
await self._device.async_play()
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@catch_request_errors
|
||||
async def async_media_stop(self) -> None:
|
||||
"""Send stop command."""
|
||||
assert self._device is not None
|
||||
await self._device.async_stop()
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@catch_request_errors
|
||||
async def async_media_seek(self, position: float) -> None:
|
||||
"""Send seek command."""
|
||||
@@ -650,6 +656,7 @@ class DlnaDmrEntity(MediaPlayerEntity):
|
||||
time = timedelta(seconds=position)
|
||||
await self._device.async_seek_rel_time(time)
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@catch_request_errors
|
||||
async def async_play_media(
|
||||
self, media_type: MediaType | str, media_id: str, **kwargs: Any
|
||||
@@ -718,12 +725,14 @@ class DlnaDmrEntity(MediaPlayerEntity):
|
||||
await self._device.async_wait_for_can_play()
|
||||
await self.async_media_play()
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@catch_request_errors
|
||||
async def async_media_previous_track(self) -> None:
|
||||
"""Send previous track command."""
|
||||
assert self._device is not None
|
||||
await self._device.async_previous()
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@catch_request_errors
|
||||
async def async_media_next_track(self) -> None:
|
||||
"""Send next track command."""
|
||||
@@ -744,6 +753,7 @@ class DlnaDmrEntity(MediaPlayerEntity):
|
||||
|
||||
return play_mode in (PlayMode.SHUFFLE, PlayMode.RANDOM)
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@catch_request_errors
|
||||
async def async_set_shuffle(self, shuffle: bool) -> None:
|
||||
"""Enable/disable shuffle mode."""
|
||||
@@ -783,6 +793,7 @@ class DlnaDmrEntity(MediaPlayerEntity):
|
||||
|
||||
return RepeatMode.OFF
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@catch_request_errors
|
||||
async def async_set_repeat(self, repeat: RepeatMode) -> None:
|
||||
"""Set repeat mode."""
|
||||
@@ -809,6 +820,7 @@ class DlnaDmrEntity(MediaPlayerEntity):
|
||||
return None
|
||||
return self._device.preset_names
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@catch_request_errors
|
||||
async def async_select_sound_mode(self, sound_mode: str) -> None:
|
||||
"""Select sound mode."""
|
||||
|
||||
@@ -94,6 +94,7 @@ class DoorBirdCamera(DoorBirdEntity, Camera):
|
||||
self._last_image = await self._door_station.device.get_image(
|
||||
self._url, timeout=_TIMEOUT
|
||||
)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except TimeoutError:
|
||||
_LOGGER.error("DoorBird %s: Camera image timed out", self.name)
|
||||
return self._last_image
|
||||
|
||||
@@ -132,6 +132,7 @@ class EgardiaAlarm(AlarmControlPanelEntity):
|
||||
"""Send disarm command."""
|
||||
try:
|
||||
self._egardiasystem.alarm_disarm()
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except requests.exceptions.RequestException as err:
|
||||
_LOGGER.error(
|
||||
"Egardia device exception occurred when sending disarm command: %s",
|
||||
@@ -142,6 +143,7 @@ class EgardiaAlarm(AlarmControlPanelEntity):
|
||||
"""Send arm home command."""
|
||||
try:
|
||||
self._egardiasystem.alarm_arm_home()
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except requests.exceptions.RequestException as err:
|
||||
_LOGGER.error(
|
||||
"Egardia device exception occurred when sending arm home command: %s",
|
||||
@@ -152,6 +154,7 @@ class EgardiaAlarm(AlarmControlPanelEntity):
|
||||
"""Send arm away command."""
|
||||
try:
|
||||
self._egardiasystem.alarm_arm_away()
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except requests.exceptions.RequestException as err:
|
||||
_LOGGER.error(
|
||||
"Egardia device exception occurred when sending arm away command: %s",
|
||||
|
||||
@@ -68,6 +68,7 @@ class Enigma2Device(CoordinatorEntity[Enigma2UpdateCoordinator], MediaPlayerEnti
|
||||
async def async_turn_off(self) -> None:
|
||||
"""Turn off media player."""
|
||||
if self.coordinator.device.turn_off_to_deep:
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
with contextlib.suppress(ServerDisconnectedError):
|
||||
await self.coordinator.device.set_powerstate(PowerState.DEEP_STANDBY)
|
||||
self._attr_available = False
|
||||
|
||||
@@ -194,6 +194,7 @@ class Eq3Climate(Eq3Entity, ClimateEntity):
|
||||
|
||||
try:
|
||||
await self._thermostat.async_set_temperature(temperature)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except Eq3Exception:
|
||||
_LOGGER.error(
|
||||
"[%s] Failed setting temperature", self._eq3_config.mac_address
|
||||
@@ -211,6 +212,7 @@ class Eq3Climate(Eq3Entity, ClimateEntity):
|
||||
|
||||
try:
|
||||
await self._thermostat.async_set_mode(HA_TO_EQ_HVAC[hvac_mode])
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except Eq3Exception:
|
||||
_LOGGER.error("[%s] Failed setting HVAC mode", self._eq3_config.mac_address)
|
||||
|
||||
|
||||
@@ -63,5 +63,6 @@ class FlockNotificationService(BaseNotificationService):
|
||||
response.status,
|
||||
result,
|
||||
)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except TimeoutError:
|
||||
_LOGGER.error("Timeout accessing Flock at %s", self._url)
|
||||
|
||||
@@ -135,6 +135,7 @@ class GenericCamera(Camera):
|
||||
return None
|
||||
try:
|
||||
url = self._still_image_url.async_render(parse_result=False)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except TemplateError as err:
|
||||
_LOGGER.error("Error parsing template %s: %s", self._still_image_url, err)
|
||||
return self._last_image
|
||||
|
||||
@@ -289,6 +289,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
|
||||
"""Service handler for reloading core config."""
|
||||
try:
|
||||
conf = await conf_util.async_hass_config_yaml(hass)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error(err)
|
||||
return
|
||||
|
||||
@@ -183,6 +183,7 @@ async def async_setup_platform(
|
||||
"""Reload the scene config."""
|
||||
try:
|
||||
config = await conf_util.async_hass_config_yaml(hass)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error(err)
|
||||
return
|
||||
|
||||
@@ -149,6 +149,7 @@ class HorizonDevice(MediaPlayerEntity):
|
||||
try:
|
||||
self._select_channel(int(media_id))
|
||||
self._attr_state = MediaPlayerState.PLAYING
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except ValueError:
|
||||
_LOGGER.error("Invalid channel: %s", media_id)
|
||||
else:
|
||||
|
||||
@@ -60,5 +60,6 @@ class HuaweiLteSmsNotificationService(BaseNotificationService):
|
||||
phone_numbers=targets, message=message
|
||||
)
|
||||
_LOGGER.debug("Sent to %s: %s", targets, resp)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except ResponseErrorException as ex:
|
||||
_LOGGER.error("Could not send to %s: %s", targets, ex)
|
||||
|
||||
@@ -82,6 +82,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
res = pyfttt.send_event(key, event, value1, value2, value3)
|
||||
if res.status_code != HTTPStatus.OK:
|
||||
_LOGGER.error("IFTTT reported error sending event to %s", target)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except requests.exceptions.RequestException:
|
||||
_LOGGER.exception("Error communicating with IFTTT")
|
||||
|
||||
|
||||
@@ -111,6 +111,7 @@ class KiwiLock(LockEntity):
|
||||
|
||||
try:
|
||||
self._client.open_door(self.lock_id)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except KiwiException:
|
||||
_LOGGER.error("Failed to open door")
|
||||
else:
|
||||
|
||||
@@ -120,6 +120,7 @@ async def service_event_register_modify(call: ServiceCall) -> None:
|
||||
for group_address in group_addresses:
|
||||
try:
|
||||
knx_module.knx_event_callback.group_addresses.remove(group_address)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except ValueError:
|
||||
_LOGGER.warning(
|
||||
"Service event_register could not remove event for '%s'",
|
||||
|
||||
@@ -102,5 +102,6 @@ class KodiNotificationService(BaseNotificationService):
|
||||
title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
|
||||
await self._server.GUI.ShowNotification(title, message, icon, displaytime)
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except jsonrpc_async.TransportError:
|
||||
_LOGGER.warning("Unable to fetch Kodi data. Is Kodi online?")
|
||||
|
||||
@@ -185,6 +185,7 @@ class ThinQFanEntity(ThinQEntity, FanEntity):
|
||||
value = percentage_to_ordered_list_item(
|
||||
self._ordered_named_fan_speeds, percentage
|
||||
)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except ValueError:
|
||||
_LOGGER.exception("Failed to async_set_percentage")
|
||||
return
|
||||
|
||||
@@ -91,5 +91,6 @@ class LifxCloudScene(Scene):
|
||||
async with asyncio.timeout(self._timeout):
|
||||
await httpsession.put(url, headers=self._headers)
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except TimeoutError, aiohttp.ClientError:
|
||||
_LOGGER.exception("Error on %s", url)
|
||||
|
||||
@@ -67,6 +67,7 @@ class LocalFile(Camera):
|
||||
try:
|
||||
with open(self._file_path, "rb") as file:
|
||||
return file.read()
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except FileNotFoundError:
|
||||
_LOGGER.warning(
|
||||
"Could not read camera %s image from file: %s",
|
||||
|
||||
@@ -127,6 +127,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Reload yaml resources."""
|
||||
try:
|
||||
conf = await async_hass_config_yaml(hass)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error(err)
|
||||
return
|
||||
|
||||
@@ -360,6 +360,7 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
|
||||
heat_setpoint=target_temp_low,
|
||||
mode=mode,
|
||||
)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except LYRIC_EXCEPTIONS as exception:
|
||||
_LOGGER.error(exception)
|
||||
await self.coordinator.async_refresh()
|
||||
@@ -388,6 +389,7 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
|
||||
await self._async_set_hvac_mode_tcc(hvac_mode)
|
||||
case LyricThermostatType.LCC:
|
||||
await self._async_set_hvac_mode_lcc(hvac_mode)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except LYRIC_EXCEPTIONS as exception:
|
||||
_LOGGER.error(exception)
|
||||
await self.coordinator.async_refresh()
|
||||
@@ -466,6 +468,7 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
|
||||
await self._update_thermostat(
|
||||
self.location, self.device, thermostat_setpoint_status=preset_mode
|
||||
)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except LYRIC_EXCEPTIONS as exception:
|
||||
_LOGGER.error(exception)
|
||||
await self.coordinator.async_refresh()
|
||||
@@ -480,6 +483,7 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
|
||||
thermostat_setpoint_status=PRESET_HOLD_UNTIL,
|
||||
next_period_time=time_period,
|
||||
)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except LYRIC_EXCEPTIONS as exception:
|
||||
_LOGGER.error(exception)
|
||||
await self.coordinator.async_refresh()
|
||||
@@ -492,6 +496,7 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
|
||||
await self._update_fan(
|
||||
self.location, self.device, mode=LYRIC_FAN_MODES[fan_mode]
|
||||
)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except LYRIC_EXCEPTIONS as exception:
|
||||
_LOGGER.error(exception)
|
||||
except KeyError:
|
||||
|
||||
@@ -52,6 +52,7 @@ class MadvrRemote(MadVREntity, RemoteEntity):
|
||||
_LOGGER.debug("Turning off")
|
||||
try:
|
||||
await self.madvr_client.power_off()
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except (ConnectionError, NotImplementedError) as err:
|
||||
_LOGGER.error("Failed to turn off device %s", err)
|
||||
|
||||
@@ -61,6 +62,7 @@ class MadvrRemote(MadVREntity, RemoteEntity):
|
||||
|
||||
try:
|
||||
await self.madvr_client.power_on(mac=self.coordinator.mac)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except (ConnectionError, NotImplementedError) as err:
|
||||
_LOGGER.error("Failed to turn on device %s", err)
|
||||
|
||||
@@ -69,5 +71,6 @@ class MadvrRemote(MadVREntity, RemoteEntity):
|
||||
_LOGGER.debug("adding command %s", command)
|
||||
try:
|
||||
await self.madvr_client.add_command_to_queue(command)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except (ConnectionError, NotImplementedError) as err:
|
||||
_LOGGER.error("Failed to send command %s", err)
|
||||
|
||||
@@ -111,5 +111,6 @@ class MailgunNotificationService(BaseNotificationService):
|
||||
files=files,
|
||||
)
|
||||
_LOGGER.debug("Message sent: %s", resp)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except MailgunError:
|
||||
_LOGGER.exception("Failed to send message")
|
||||
|
||||
@@ -65,6 +65,7 @@ class MessageBirdNotificationService(BaseNotificationService):
|
||||
self.client.message_create(
|
||||
self.sender, target, message, {"reference": "HA"}
|
||||
)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except ErrorException as exception:
|
||||
_LOGGER.error("Failed to notify %s: %s", target, exception)
|
||||
continue
|
||||
|
||||
@@ -115,6 +115,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
entities[g_id] = MicrosoftFaceGroupEntity(face, g_id, name)
|
||||
await component.async_add_entities([entities[g_id]])
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error("Can't create group '%s' with error: %s", g_id, err)
|
||||
|
||||
@@ -132,6 +133,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
entity = entities.pop(g_id)
|
||||
await component.async_remove_entity(entity.entity_id)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error("Can't delete group '%s' with error: %s", g_id, err)
|
||||
|
||||
@@ -145,6 +147,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
try:
|
||||
await face.call_api("post", f"persongroups/{g_id}/train")
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error("Can't train group '%s' with error: %s", g_id, err)
|
||||
|
||||
@@ -164,6 +167,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
face.store[g_id][name] = user_data["personId"]
|
||||
entities[g_id].async_write_ha_state()
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error("Can't create person '%s' with error: %s", name, err)
|
||||
|
||||
@@ -182,6 +186,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
face.store[g_id].pop(name)
|
||||
entities[g_id].async_write_ha_state()
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error("Can't delete person '%s' with error: %s", p_id, err)
|
||||
|
||||
@@ -205,6 +210,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
image.content,
|
||||
binary=True,
|
||||
)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error(
|
||||
"Can't add an image of a person '%s' with error: %s", p_id, err
|
||||
|
||||
@@ -146,6 +146,7 @@ class MjpegCamera(Camera):
|
||||
|
||||
return await response.read()
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except TimeoutError:
|
||||
LOGGER.error("Timeout getting camera image from %s", self.name)
|
||||
|
||||
|
||||
@@ -118,6 +118,7 @@ class MochadLight(LightEntity):
|
||||
self._adjust_brightness(brightness)
|
||||
self._attr_brightness = brightness
|
||||
self._attr_is_on = True
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except (MochadException, OSError) as exc:
|
||||
_LOGGER.error("Error with mochad communication: %s", exc)
|
||||
|
||||
@@ -135,5 +136,6 @@ class MochadLight(LightEntity):
|
||||
if self._brightness_levels == 31:
|
||||
self._attr_brightness = 0
|
||||
self._attr_is_on = False
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except (MochadException, OSError) as exc:
|
||||
_LOGGER.error("Error with mochad communication: %s", exc)
|
||||
|
||||
@@ -78,6 +78,7 @@ class MochadSwitch(SwitchEntity):
|
||||
if self._comm_type == "pl":
|
||||
self._controller.read_data()
|
||||
self._attr_is_on = True
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except (MochadException, OSError) as exc:
|
||||
_LOGGER.error("Error with mochad communication: %s", exc)
|
||||
|
||||
@@ -94,6 +95,7 @@ class MochadSwitch(SwitchEntity):
|
||||
if self._comm_type == "pl":
|
||||
self._controller.read_data()
|
||||
self._attr_is_on = False
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except (MochadException, OSError) as exc:
|
||||
_LOGGER.error("Error with mochad communication: %s", exc)
|
||||
|
||||
|
||||
@@ -108,11 +108,13 @@ class ModernFormsFanEntity(FanEntity, ModernFormsDeviceEntity):
|
||||
"""Return the state of the fan."""
|
||||
return bool(self.coordinator.data.state.fan_on)
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@modernforms_exception_handler
|
||||
async def async_set_direction(self, direction: str) -> None:
|
||||
"""Set the direction of the fan."""
|
||||
await self.coordinator.modern_forms.fan(direction=direction)
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@modernforms_exception_handler
|
||||
async def async_set_percentage(self, percentage: int) -> None:
|
||||
"""Set the speed percentage of the fan."""
|
||||
@@ -121,6 +123,7 @@ class ModernFormsFanEntity(FanEntity, ModernFormsDeviceEntity):
|
||||
else:
|
||||
await self.async_turn_off()
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@modernforms_exception_handler
|
||||
async def async_turn_on(
|
||||
self,
|
||||
@@ -137,11 +140,13 @@ class ModernFormsFanEntity(FanEntity, ModernFormsDeviceEntity):
|
||||
)
|
||||
await self.coordinator.modern_forms.fan(**data)
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@modernforms_exception_handler
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the fan off."""
|
||||
await self.coordinator.modern_forms.fan(on=FAN_POWER_OFF)
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@modernforms_exception_handler
|
||||
async def async_set_fan_sleep_timer(
|
||||
self,
|
||||
@@ -150,6 +155,7 @@ class ModernFormsFanEntity(FanEntity, ModernFormsDeviceEntity):
|
||||
"""Set a Modern Forms light sleep timer."""
|
||||
await self.coordinator.modern_forms.fan(sleep=sleep_time * 60)
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@modernforms_exception_handler
|
||||
async def async_clear_fan_sleep_timer(
|
||||
self,
|
||||
|
||||
@@ -100,11 +100,13 @@ class ModernFormsLightEntity(ModernFormsDeviceEntity, LightEntity):
|
||||
"""Return the state of the light."""
|
||||
return bool(self.coordinator.data.state.light_on)
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@modernforms_exception_handler
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off the light."""
|
||||
await self.coordinator.modern_forms.light(on=LIGHT_POWER_OFF)
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@modernforms_exception_handler
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on the light."""
|
||||
@@ -117,6 +119,7 @@ class ModernFormsLightEntity(ModernFormsDeviceEntity, LightEntity):
|
||||
|
||||
await self.coordinator.modern_forms.light(**data)
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@modernforms_exception_handler
|
||||
async def async_set_light_sleep_timer(
|
||||
self,
|
||||
@@ -125,6 +128,7 @@ class ModernFormsLightEntity(ModernFormsDeviceEntity, LightEntity):
|
||||
"""Set a Modern Forms light sleep timer."""
|
||||
await self.coordinator.modern_forms.light(sleep=sleep_time * 60)
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@modernforms_exception_handler
|
||||
async def async_clear_light_sleep_timer(
|
||||
self,
|
||||
|
||||
@@ -62,11 +62,13 @@ class ModernFormsAwaySwitch(ModernFormsSwitch):
|
||||
"""Return the state of the switch."""
|
||||
return bool(self.coordinator.data.state.away_mode_enabled)
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@modernforms_exception_handler
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off the Modern Forms Away mode switch."""
|
||||
await self.coordinator.modern_forms.away(away=False)
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@modernforms_exception_handler
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on the Modern Forms Away mode switch."""
|
||||
@@ -93,11 +95,13 @@ class ModernFormsAdaptiveLearningSwitch(ModernFormsSwitch):
|
||||
"""Return the state of the switch."""
|
||||
return bool(self.coordinator.data.state.adaptive_learning_enabled)
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@modernforms_exception_handler
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off the Modern Forms Adaptive Learning switch."""
|
||||
await self.coordinator.modern_forms.adaptive_learning(adaptive_learning=False)
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@modernforms_exception_handler
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on the Modern Forms Adaptive Learning switch."""
|
||||
|
||||
@@ -70,5 +70,6 @@ class MSTeamsNotificationService(BaseNotificationService):
|
||||
teams_message.addSection(message_section)
|
||||
try:
|
||||
teams_message.send()
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except RuntimeError as err:
|
||||
_LOGGER.error("Could not send notification. Error: %s", err)
|
||||
|
||||
@@ -89,6 +89,7 @@ class MyStromLight(LightEntity):
|
||||
await self._bulb.set_sunrise(30)
|
||||
if effect == EFFECT_RAINBOW:
|
||||
await self._bulb.set_rainbow(30)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except MyStromConnectionError:
|
||||
_LOGGER.warning("No route to myStrom bulb")
|
||||
|
||||
@@ -96,6 +97,7 @@ class MyStromLight(LightEntity):
|
||||
"""Turn off the bulb."""
|
||||
try:
|
||||
await self._bulb.set_off()
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except MyStromConnectionError:
|
||||
_LOGGER.warning("The myStrom bulb not online")
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ class MyStromSwitch(SwitchEntity):
|
||||
"""Turn the switch on."""
|
||||
try:
|
||||
await self.plug.turn_on()
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except MyStromConnectionError:
|
||||
_LOGGER.error("No route to myStrom plug")
|
||||
|
||||
@@ -58,6 +59,7 @@ class MyStromSwitch(SwitchEntity):
|
||||
"""Turn the switch off."""
|
||||
try:
|
||||
await self.plug.turn_off()
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except MyStromConnectionError:
|
||||
_LOGGER.error("No route to myStrom plug")
|
||||
|
||||
|
||||
@@ -100,6 +100,7 @@ class NeatoConnectedSwitch(NeatoEntity, SwitchEntity):
|
||||
if self.type == SWITCH_TYPE_SCHEDULE:
|
||||
try:
|
||||
self.robot.enable_schedule()
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except NeatoRobotException as ex:
|
||||
_LOGGER.error(
|
||||
"Neato switch connection error '%s': %s", self.entity_id, ex
|
||||
@@ -110,6 +111,7 @@ class NeatoConnectedSwitch(NeatoEntity, SwitchEntity):
|
||||
if self.type == SWITCH_TYPE_SCHEDULE:
|
||||
try:
|
||||
self.robot.disable_schedule()
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except NeatoRobotException as ex:
|
||||
_LOGGER.error(
|
||||
"Neato switch connection error '%s': %s", self.entity_id, ex
|
||||
|
||||
@@ -275,6 +275,7 @@ class NeatoConnectedVacuum(NeatoEntity, StateVacuumEntity):
|
||||
self.robot.start_cleaning()
|
||||
elif self._state["state"] == 3:
|
||||
self.robot.resume_cleaning()
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except NeatoRobotException as ex:
|
||||
_LOGGER.error(
|
||||
"Neato vacuum connection error for '%s': %s", self.entity_id, ex
|
||||
@@ -284,6 +285,7 @@ class NeatoConnectedVacuum(NeatoEntity, StateVacuumEntity):
|
||||
"""Pause the vacuum."""
|
||||
try:
|
||||
self.robot.pause_cleaning()
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except NeatoRobotException as ex:
|
||||
_LOGGER.error(
|
||||
"Neato vacuum connection error for '%s': %s", self.entity_id, ex
|
||||
@@ -296,6 +298,7 @@ class NeatoConnectedVacuum(NeatoEntity, StateVacuumEntity):
|
||||
self.robot.pause_cleaning()
|
||||
self._attr_activity = VacuumActivity.RETURNING
|
||||
self.robot.send_to_base()
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except NeatoRobotException as ex:
|
||||
_LOGGER.error(
|
||||
"Neato vacuum connection error for '%s': %s", self.entity_id, ex
|
||||
@@ -305,6 +308,7 @@ class NeatoConnectedVacuum(NeatoEntity, StateVacuumEntity):
|
||||
"""Stop the vacuum cleaner."""
|
||||
try:
|
||||
self.robot.stop_cleaning()
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except NeatoRobotException as ex:
|
||||
_LOGGER.error(
|
||||
"Neato vacuum connection error for '%s': %s", self.entity_id, ex
|
||||
@@ -314,6 +318,7 @@ class NeatoConnectedVacuum(NeatoEntity, StateVacuumEntity):
|
||||
"""Locate the robot by making it emit a sound."""
|
||||
try:
|
||||
self.robot.locate()
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except NeatoRobotException as ex:
|
||||
_LOGGER.error(
|
||||
"Neato vacuum connection error for '%s': %s", self.entity_id, ex
|
||||
@@ -323,6 +328,7 @@ class NeatoConnectedVacuum(NeatoEntity, StateVacuumEntity):
|
||||
"""Run a spot cleaning starting from the base."""
|
||||
try:
|
||||
self.robot.start_spot_cleaning()
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except NeatoRobotException as ex:
|
||||
_LOGGER.error(
|
||||
"Neato vacuum connection error for '%s': %s", self.entity_id, ex
|
||||
|
||||
@@ -162,6 +162,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: NetatmoConfigEntry) -> b
|
||||
try:
|
||||
await entry.runtime_data.auth.async_addwebhook(webhook_url)
|
||||
_LOGGER.debug("Register Netatmo webhook: %s", webhook_url)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except pyatmo.ApiError as err:
|
||||
_LOGGER.error("Error during webhook registration - %s", err)
|
||||
else:
|
||||
|
||||
@@ -56,5 +56,6 @@ class NetgearNotifyService(BaseNotificationService):
|
||||
for target in targets:
|
||||
try:
|
||||
await self.modem.sms(target, message)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except eternalegypt.Error:
|
||||
LOGGER.error("Unable to send to %s", target)
|
||||
|
||||
@@ -103,6 +103,7 @@ class NFAndroidTVNotificationService(BaseNotificationService):
|
||||
duration = int(
|
||||
data.get(ATTR_DURATION, Notifications.DEFAULT_DURATION)
|
||||
)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except ValueError:
|
||||
_LOGGER.warning(
|
||||
"Invalid duration-value: %s", data.get(ATTR_DURATION)
|
||||
|
||||
@@ -86,6 +86,7 @@ class NumatoGpioSwitch(SwitchEntity):
|
||||
)
|
||||
self._attr_is_on = True
|
||||
self.schedule_update_ha_state()
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except NumatoGpioError as err:
|
||||
_LOGGER.error(
|
||||
"Failed to turn on Numato device %s port %s: %s",
|
||||
@@ -102,6 +103,7 @@ class NumatoGpioSwitch(SwitchEntity):
|
||||
)
|
||||
self._attr_is_on = False
|
||||
self.schedule_update_ha_state()
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except NumatoGpioError as err:
|
||||
_LOGGER.error(
|
||||
"Failed to turn off Numato device %s port %s: %s",
|
||||
|
||||
@@ -136,6 +136,7 @@ class ONVIFCameraEntity(ONVIFBaseEntity, Camera):
|
||||
self.profile.token, self._basic_auth
|
||||
):
|
||||
return image
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except ONVIFError as err:
|
||||
LOGGER.error(
|
||||
"Fetch snapshot image failed from %s, falling back to FFmpeg; %s",
|
||||
|
||||
@@ -177,6 +177,7 @@ async def _async_upload_image(call: ServiceCall) -> None:
|
||||
current = asyncio.current_task()
|
||||
if (prev := entry.runtime_data.upload_task) is not None and not prev.done():
|
||||
prev.cancel()
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
with contextlib.suppress(asyncio.CancelledError):
|
||||
await prev
|
||||
entry.runtime_data.upload_task = current
|
||||
|
||||
@@ -170,16 +170,19 @@ class OpenhomeDevice(MediaPlayerEntity):
|
||||
except TimeoutError, aiohttp.ClientError, UpnpError:
|
||||
self._attr_available = False
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@catch_request_errors()
|
||||
async def async_turn_on(self) -> None:
|
||||
"""Bring device out of standby."""
|
||||
await self._device.set_standby(False)
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@catch_request_errors()
|
||||
async def async_turn_off(self) -> None:
|
||||
"""Put device in standby."""
|
||||
await self._device.set_standby(True)
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@catch_request_errors()
|
||||
async def async_play_media(
|
||||
self, media_type: MediaType | str, media_id: str, **kwargs: Any
|
||||
@@ -205,31 +208,37 @@ class OpenhomeDevice(MediaPlayerEntity):
|
||||
track_details = {"title": "Home Assistant", "uri": media_id}
|
||||
await self._device.play_media(track_details)
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@catch_request_errors()
|
||||
async def async_media_pause(self) -> None:
|
||||
"""Send pause command."""
|
||||
await self._device.pause()
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@catch_request_errors()
|
||||
async def async_media_stop(self) -> None:
|
||||
"""Send stop command."""
|
||||
await self._device.stop()
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@catch_request_errors()
|
||||
async def async_media_play(self) -> None:
|
||||
"""Send play command."""
|
||||
await self._device.play()
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@catch_request_errors()
|
||||
async def async_media_next_track(self) -> None:
|
||||
"""Send next track command."""
|
||||
await self._device.skip(1)
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@catch_request_errors()
|
||||
async def async_media_previous_track(self) -> None:
|
||||
"""Send previous track command."""
|
||||
await self._device.skip(-1)
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@catch_request_errors()
|
||||
async def async_select_source(self, source: str) -> None:
|
||||
"""Select input source."""
|
||||
@@ -246,21 +255,25 @@ class OpenhomeDevice(MediaPlayerEntity):
|
||||
except UpnpError:
|
||||
_LOGGER.error("Error invoking pin %s", pin)
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@catch_request_errors()
|
||||
async def async_volume_up(self) -> None:
|
||||
"""Volume up media player."""
|
||||
await self._device.increase_volume()
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@catch_request_errors()
|
||||
async def async_volume_down(self) -> None:
|
||||
"""Volume down media player."""
|
||||
await self._device.decrease_volume()
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@catch_request_errors()
|
||||
async def async_set_volume_level(self, volume: float) -> None:
|
||||
"""Set volume level, range 0..1."""
|
||||
await self._device.set_volume(int(volume * 100))
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@catch_request_errors()
|
||||
async def async_mute_volume(self, mute: bool) -> None:
|
||||
"""Mute (true) or unmute (false) media player."""
|
||||
|
||||
@@ -75,6 +75,7 @@ class PiHoleSwitch(PiHoleEntity, SwitchEntity):
|
||||
try:
|
||||
await self.api.enable()
|
||||
await self.async_update()
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except HoleError as err:
|
||||
_LOGGER.error("Unable to enable Pi-hole: %s", err)
|
||||
|
||||
@@ -96,5 +97,6 @@ class PiHoleSwitch(PiHoleEntity, SwitchEntity):
|
||||
try:
|
||||
await self.api.disable(duration_seconds)
|
||||
await self.async_update()
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except HoleError as err:
|
||||
_LOGGER.error("Unable to disable Pi-hole: %s", err)
|
||||
|
||||
@@ -98,6 +98,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
try:
|
||||
pilight_client.send_code(message_data)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except OSError:
|
||||
_LOGGER.error("Pilight send failed for %s", str(message_data))
|
||||
|
||||
|
||||
@@ -81,6 +81,7 @@ class ProsegurCamera(Camera):
|
||||
try:
|
||||
return await self._installation.get_image(self._auth, self._camera.id)
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except ProsegurException as err:
|
||||
_LOGGER.error("Image %s doesn't exist: %s", self._camera.description, err)
|
||||
|
||||
@@ -93,6 +94,7 @@ class ProsegurCamera(Camera):
|
||||
try:
|
||||
await self._installation.request_image(self._auth, self._camera.id)
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except ProsegurException as err:
|
||||
_LOGGER.error(
|
||||
"Could not request image from camera %s: %s",
|
||||
|
||||
@@ -89,6 +89,7 @@ class PushoverNotificationService(BaseNotificationService):
|
||||
file_handle = open(data[ATTR_ATTACHMENT], "rb")
|
||||
# Replace the attachment identifier with file object.
|
||||
image = file_handle
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except OSError as ex_val:
|
||||
_LOGGER.error(ex_val)
|
||||
# Remove attachment key to send without attachment.
|
||||
|
||||
@@ -98,6 +98,7 @@ class QVRProCamera(Camera):
|
||||
try:
|
||||
return self._client.get_snapshot(self.guid)
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except QVRResponseError as ex:
|
||||
_LOGGER.error("Error getting image: %s", ex)
|
||||
self._client.connect()
|
||||
|
||||
@@ -75,6 +75,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
async def reload_service_handler(service: ServiceCall) -> None:
|
||||
"""Remove all user-defined groups and load new ones from config."""
|
||||
conf = None
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
with contextlib.suppress(HomeAssistantError):
|
||||
conf = await async_integration_yaml_config(hass, DOMAIN)
|
||||
if conf is None:
|
||||
|
||||
@@ -170,6 +170,7 @@ class RestSwitch(ManualTriggerEntity, SwitchEntity):
|
||||
_LOGGER.error(
|
||||
"Can't turn on %s. Is resource/endpoint offline?", self._resource
|
||||
)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except TimeoutError, httpx.RequestError:
|
||||
_LOGGER.error("Error while switching on %s", self._resource)
|
||||
|
||||
@@ -185,6 +186,7 @@ class RestSwitch(ManualTriggerEntity, SwitchEntity):
|
||||
_LOGGER.error(
|
||||
"Can't turn off %s. Is resource/endpoint offline?", self._resource
|
||||
)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except TimeoutError, httpx.RequestError:
|
||||
_LOGGER.error("Error while switching off %s", self._resource)
|
||||
|
||||
|
||||
@@ -354,6 +354,7 @@ class BraavaJet(IRobotVacuum):
|
||||
spray = int(split[1])
|
||||
if behavior.capitalize() in BRAAVA_MOP_BEHAVIORS:
|
||||
behavior = behavior.capitalize()
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except IndexError:
|
||||
_LOGGER.error(
|
||||
"Fan speed error: expected {behavior}-{spray_amount}, got '%s'",
|
||||
|
||||
@@ -366,6 +366,7 @@ class SamsungTVDevice(SamsungTVEntity, MediaPlayerEntity):
|
||||
# media_id should only be a channel number
|
||||
try:
|
||||
cv.positive_int(media_id)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except vol.Invalid:
|
||||
LOGGER.error("Media ID must be positive integer")
|
||||
return
|
||||
|
||||
@@ -135,5 +135,6 @@ class SchluterThermostat(CoordinatorEntity, ClimateEntity):
|
||||
try:
|
||||
if target_temp is not None:
|
||||
self._api.set_temperature(self._session_id, serial_number, target_temp)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except RequestException as ex:
|
||||
_LOGGER.error("An error occurred while setting temperature: %s", ex)
|
||||
|
||||
@@ -189,6 +189,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Reload shell_command from YAML configuration."""
|
||||
try:
|
||||
raw_config = await conf_util.async_hass_config_yaml(hass)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error("Error loading configuration.yaml: %s", err)
|
||||
return
|
||||
|
||||
@@ -100,6 +100,7 @@ async def async_setup_entry(
|
||||
|
||||
try:
|
||||
item = [item for item in data.items if item["name"] == name][0]
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except IndexError:
|
||||
_LOGGER.error("Removing of item failed: %s cannot be found", name)
|
||||
else:
|
||||
@@ -110,6 +111,7 @@ async def async_setup_entry(
|
||||
name = call.data[ATTR_NAME]
|
||||
try:
|
||||
await config_entry.runtime_data.async_complete(name)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except NoMatchingShoppingListItem:
|
||||
_LOGGER.error("Completing of item failed: %s cannot be found", name)
|
||||
|
||||
@@ -120,6 +122,7 @@ async def async_setup_entry(
|
||||
|
||||
try:
|
||||
item = [item for item in data.items if item["name"] == name][0]
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except IndexError:
|
||||
_LOGGER.error("Restoring of item failed: %s cannot be found", name)
|
||||
else:
|
||||
|
||||
@@ -96,6 +96,7 @@ class SimplePushNotificationService(BaseNotificationService):
|
||||
event=event,
|
||||
)
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except BadRequest:
|
||||
_LOGGER.error("Bad request. Title or message are too long")
|
||||
except UnknownError:
|
||||
|
||||
@@ -89,6 +89,7 @@ class SinchNotificationService(BaseNotificationService):
|
||||
_LOGGER.debug(
|
||||
'Successfully sent SMS to "%s" (batch_id: %s)', target, batch_id
|
||||
)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except ErrorResponseException as ex:
|
||||
_LOGGER.error(
|
||||
"Caught ErrorResponseException. Response code: %s (%s)",
|
||||
|
||||
@@ -64,6 +64,7 @@ class SkyRemote(RemoteEntity):
|
||||
)
|
||||
try:
|
||||
self._remote.send_keys(command)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except ValueError as err:
|
||||
_LOGGER.error("Invalid command: %s. Error: %s", command, err)
|
||||
return
|
||||
|
||||
@@ -277,6 +277,7 @@ class SlackNotificationService(BaseNotificationService):
|
||||
|
||||
try:
|
||||
DATA_SCHEMA(data)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except vol.Invalid as err:
|
||||
_LOGGER.error("Invalid message data: %s", err)
|
||||
data = {}
|
||||
|
||||
@@ -125,6 +125,7 @@ class SmLightEntity(SmEntity, LightEntity):
|
||||
effect_name: str = kwargs[ATTR_EFFECT]
|
||||
try:
|
||||
idx = self.entity_description.effect_list.index(effect_name)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except ValueError:
|
||||
_LOGGER.warning("Unknown effect: %s", effect_name)
|
||||
return
|
||||
|
||||
@@ -241,6 +241,7 @@ class SmUpdateEntity(SmEntity, UpdateEntity):
|
||||
):
|
||||
await self.coordinator.async_refresh()
|
||||
await asyncio.sleep(1)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except TimeoutError:
|
||||
LOGGER.warning(
|
||||
"Timeout waiting for %s to reboot after update",
|
||||
|
||||
@@ -58,6 +58,7 @@ async def _service_handler(call: ServiceCall) -> None:
|
||||
dsm_api = dsm_device.api
|
||||
try:
|
||||
await getattr(dsm_api, f"async_{call.service}")()
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except SynologyDSMException as ex:
|
||||
LOGGER.error(
|
||||
"%s of DSM with serial %s not possible, because of %s",
|
||||
|
||||
@@ -77,6 +77,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
await async_get_blueprints(hass).async_reset_cache()
|
||||
try:
|
||||
unprocessed_conf = await conf_util.async_hass_config_yaml(hass)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error(err)
|
||||
return
|
||||
|
||||
@@ -102,12 +102,14 @@ class ToonThermostatDevice(ToonDisplayDeviceEntity, ClimateEntity):
|
||||
"""Return the current state of the burner."""
|
||||
return {"heating_type": self.coordinator.data.agreement.heating_type}
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@toon_exception_handler
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Change the setpoint of the thermostat."""
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
await self.coordinator.toon.set_current_setpoint(temperature)
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@toon_exception_handler
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set new preset mode."""
|
||||
|
||||
@@ -60,6 +60,7 @@ class ToonSwitch(ToonEntity, SwitchEntity):
|
||||
class ToonProgramSwitch(ToonSwitch, ToonDisplayDeviceEntity):
|
||||
"""Defines a Toon program switch."""
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@toon_exception_handler
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off the Toon program switch."""
|
||||
@@ -67,6 +68,7 @@ class ToonProgramSwitch(ToonSwitch, ToonDisplayDeviceEntity):
|
||||
ACTIVE_STATE_AWAY, PROGRAM_STATE_OFF
|
||||
)
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@toon_exception_handler
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on the Toon program switch."""
|
||||
@@ -78,6 +80,7 @@ class ToonProgramSwitch(ToonSwitch, ToonDisplayDeviceEntity):
|
||||
class ToonHolidayModeSwitch(ToonSwitch, ToonDisplayDeviceEntity):
|
||||
"""Defines a Toon Holiday mode switch."""
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@toon_exception_handler
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off the Toon holiday mode switch."""
|
||||
@@ -85,6 +88,7 @@ class ToonHolidayModeSwitch(ToonSwitch, ToonDisplayDeviceEntity):
|
||||
ACTIVE_STATE_AWAY, PROGRAM_STATE_ON
|
||||
)
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@toon_exception_handler
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on the Toon holiday mode switch."""
|
||||
|
||||
@@ -110,6 +110,7 @@ class TractiveSwitch(TractiveEntity, SwitchEntity):
|
||||
"""Turn on a switch."""
|
||||
try:
|
||||
result = await self._method(True)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except TractiveError as error:
|
||||
_LOGGER.error(error)
|
||||
return
|
||||
@@ -122,6 +123,7 @@ class TractiveSwitch(TractiveEntity, SwitchEntity):
|
||||
"""Turn off a switch."""
|
||||
try:
|
||||
result = await self._method(False)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except TractiveError as error:
|
||||
_LOGGER.error(error)
|
||||
return
|
||||
|
||||
@@ -66,5 +66,6 @@ class TwilioCallNotificationService(BaseNotificationService):
|
||||
self.client.calls.create(
|
||||
to=target, url=twimlet_url, from_=self.from_number
|
||||
)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except TwilioRestException as exc:
|
||||
_LOGGER.error(exc)
|
||||
|
||||
@@ -75,6 +75,7 @@ class UnifiAccessDoorImageEntity(UnifiAccessEntity, ImageEntity):
|
||||
if thumbnail := self.coordinator.data.door_thumbnails.get(self._door_id):
|
||||
try:
|
||||
return await self.coordinator.client.get_thumbnail(thumbnail.url)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except UnifiAccessError as err:
|
||||
_LOGGER.warning(
|
||||
"Failed to fetch thumbnail for door %s: %s",
|
||||
|
||||
@@ -96,6 +96,7 @@ async def _async_set_profile(call: ServiceCall) -> None:
|
||||
await coordinator.client.set_profile(
|
||||
I18N_KEY_TO_VALLOX_PROFILE[profile_key], duration
|
||||
)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except ValloxApiException as err:
|
||||
_LOGGER.error(
|
||||
"Error setting profile %s for duration %s: %s",
|
||||
|
||||
@@ -100,6 +100,7 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
||||
shutil.rmtree,
|
||||
hass.config.path(STORAGE_DIR, f"velbuscache-{entry.entry_id}/"),
|
||||
)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except FileNotFoundError:
|
||||
pass # It's okay if the file doesn't exist
|
||||
except OSError as exc:
|
||||
|
||||
@@ -183,11 +183,13 @@ class VlcDevice(MediaPlayerEntity):
|
||||
else:
|
||||
self._attr_media_title = media_title
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@catch_vlc_errors
|
||||
async def async_media_seek(self, position: float) -> None:
|
||||
"""Seek the media to a specific location."""
|
||||
await self._vlc.seek(round(position))
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@catch_vlc_errors
|
||||
async def async_mute_volume(self, mute: bool) -> None:
|
||||
"""Mute the volume."""
|
||||
@@ -200,6 +202,7 @@ class VlcDevice(MediaPlayerEntity):
|
||||
|
||||
self._attr_is_volume_muted = mute
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@catch_vlc_errors
|
||||
async def async_set_volume_level(self, volume: float) -> None:
|
||||
"""Set volume level, range 0..1."""
|
||||
@@ -210,6 +213,7 @@ class VlcDevice(MediaPlayerEntity):
|
||||
# This can happen if we were muted and then see a volume_up.
|
||||
self._attr_is_volume_muted = False
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@catch_vlc_errors
|
||||
async def async_media_play(self) -> None:
|
||||
"""Send play command."""
|
||||
@@ -221,6 +225,7 @@ class VlcDevice(MediaPlayerEntity):
|
||||
await self._vlc.play()
|
||||
self._attr_state = MediaPlayerState.PLAYING
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@catch_vlc_errors
|
||||
async def async_media_pause(self) -> None:
|
||||
"""Send pause command."""
|
||||
@@ -231,12 +236,14 @@ class VlcDevice(MediaPlayerEntity):
|
||||
|
||||
self._attr_state = MediaPlayerState.PAUSED
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@catch_vlc_errors
|
||||
async def async_media_stop(self) -> None:
|
||||
"""Send stop command."""
|
||||
await self._vlc.stop()
|
||||
self._attr_state = MediaPlayerState.IDLE
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@catch_vlc_errors
|
||||
async def async_play_media(
|
||||
self, media_type: MediaType | str, media_id: str, **kwargs: Any
|
||||
@@ -257,21 +264,25 @@ class VlcDevice(MediaPlayerEntity):
|
||||
await self._vlc.add(media_id)
|
||||
self._attr_state = MediaPlayerState.PLAYING
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@catch_vlc_errors
|
||||
async def async_media_previous_track(self) -> None:
|
||||
"""Send previous track command."""
|
||||
await self._vlc.prev()
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@catch_vlc_errors
|
||||
async def async_media_next_track(self) -> None:
|
||||
"""Send next track command."""
|
||||
await self._vlc.next()
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@catch_vlc_errors
|
||||
async def async_clear_playlist(self) -> None:
|
||||
"""Clear players playlist."""
|
||||
await self._vlc.clear()
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
@catch_vlc_errors
|
||||
async def async_set_shuffle(self, shuffle: bool) -> None:
|
||||
"""Enable/disable shuffle mode."""
|
||||
|
||||
@@ -131,6 +131,7 @@ class XeomaCamera(Camera):
|
||||
self._image, self._username, self._password
|
||||
)
|
||||
self._last_image = image
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except XeomaError as err:
|
||||
_LOGGER.error("Error fetching image: %s", err.message)
|
||||
|
||||
|
||||
@@ -155,6 +155,7 @@ class XiaomiCamera(Camera):
|
||||
|
||||
try:
|
||||
host = self.host.async_render(parse_result=False)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except TemplateError as exc:
|
||||
_LOGGER.error("Error parsing template %s: %s", self.host, exc)
|
||||
return self._last_image
|
||||
|
||||
@@ -198,6 +198,7 @@ class MiroboVacuum(
|
||||
else:
|
||||
try:
|
||||
fan_speed_int = int(fan_speed)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except ValueError as exc:
|
||||
_LOGGER.error(
|
||||
"Fan speed step not recognized (%s). Valid speeds are: %s",
|
||||
|
||||
@@ -407,6 +407,7 @@ class YamahaDeviceZone(MediaPlayerEntity):
|
||||
"""Set the current scene."""
|
||||
try:
|
||||
self.zctrl.scene = scene
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except AssertionError:
|
||||
_LOGGER.warning("Scene '%s' does not exist!", scene)
|
||||
|
||||
|
||||
@@ -371,6 +371,7 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity):
|
||||
]
|
||||
|
||||
if add_media_source:
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
with contextlib.suppress(BrowseError):
|
||||
item = await media_source.async_browse_media(
|
||||
self.hass,
|
||||
@@ -746,6 +747,7 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity):
|
||||
if client != self:
|
||||
try:
|
||||
network_join = await client.async_client_join(group, self)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except MusicCastGroupException:
|
||||
_LOGGER.warning(
|
||||
(
|
||||
|
||||
@@ -595,6 +595,7 @@ class YeelightBaseLight(YeelightEntity, LightEntity):
|
||||
"""Set the music mode on or off."""
|
||||
try:
|
||||
await self._async_set_music_mode(music_mode)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except AssertionError as ex:
|
||||
_LOGGER.error("Unable to turn on music mode, consider disabling it: %s", ex)
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
"""Action/service related checkers."""
|
||||
@@ -0,0 +1,325 @@
|
||||
"""Constants for action/service checkers."""
|
||||
|
||||
from pylint_home_assistant.const import Platform
|
||||
|
||||
# Entity action methods per platform. Includes both the handler names
|
||||
# registered via async_register_entity_service AND the entity methods
|
||||
# those handlers delegate to (which integrations override).
|
||||
PLATFORM_ACTION_METHODS: dict[str, set[str]] = {
|
||||
Platform.ALARM_CONTROL_PANEL: {
|
||||
"alarm_arm_away",
|
||||
"alarm_arm_custom_bypass",
|
||||
"alarm_arm_home",
|
||||
"alarm_arm_night",
|
||||
"alarm_arm_vacation",
|
||||
"alarm_disarm",
|
||||
"alarm_trigger",
|
||||
"async_alarm_arm_away",
|
||||
"async_alarm_arm_custom_bypass",
|
||||
"async_alarm_arm_home",
|
||||
"async_alarm_arm_night",
|
||||
"async_alarm_arm_vacation",
|
||||
"async_alarm_disarm",
|
||||
"async_alarm_trigger",
|
||||
},
|
||||
Platform.ASSIST_SATELLITE: {
|
||||
"async_announce",
|
||||
"async_start_conversation",
|
||||
},
|
||||
Platform.BUTTON: {
|
||||
"async_press",
|
||||
"press",
|
||||
},
|
||||
Platform.CALENDAR: {
|
||||
"async_create_event",
|
||||
"async_delete_event",
|
||||
"async_get_events",
|
||||
"async_update_event",
|
||||
},
|
||||
Platform.CAMERA: {
|
||||
"async_camera_image",
|
||||
"async_disable_motion_detection",
|
||||
"async_enable_motion_detection",
|
||||
"async_turn_off",
|
||||
"async_turn_on",
|
||||
"camera_image",
|
||||
"disable_motion_detection",
|
||||
"enable_motion_detection",
|
||||
"turn_off",
|
||||
"turn_on",
|
||||
},
|
||||
Platform.CLIMATE: {
|
||||
"async_set_fan_mode",
|
||||
"async_set_humidity",
|
||||
"async_set_hvac_mode",
|
||||
"async_set_preset_mode",
|
||||
"async_set_swing_horizontal_mode",
|
||||
"async_set_swing_mode",
|
||||
"async_set_temperature",
|
||||
"async_toggle",
|
||||
"async_turn_off",
|
||||
"async_turn_on",
|
||||
"set_fan_mode",
|
||||
"set_humidity",
|
||||
"set_hvac_mode",
|
||||
"set_preset_mode",
|
||||
"set_swing_horizontal_mode",
|
||||
"set_swing_mode",
|
||||
"set_temperature",
|
||||
"toggle",
|
||||
"turn_off",
|
||||
"turn_on",
|
||||
},
|
||||
Platform.COVER: {
|
||||
"async_close_cover_tilt",
|
||||
"async_close_cover",
|
||||
"async_open_cover_tilt",
|
||||
"async_open_cover",
|
||||
"async_set_cover_position",
|
||||
"async_set_cover_tilt_position",
|
||||
"async_stop_cover_tilt",
|
||||
"async_stop_cover",
|
||||
"async_toggle_tilt",
|
||||
"async_toggle",
|
||||
"close_cover_tilt",
|
||||
"close_cover",
|
||||
"open_cover_tilt",
|
||||
"open_cover",
|
||||
"set_cover_position",
|
||||
"set_cover_tilt_position",
|
||||
"stop_cover_tilt",
|
||||
"stop_cover",
|
||||
"toggle_tilt",
|
||||
"toggle",
|
||||
},
|
||||
Platform.DATE: {
|
||||
"async_set_value",
|
||||
"set_value",
|
||||
},
|
||||
Platform.DATETIME: {
|
||||
"async_set_value",
|
||||
"set_value",
|
||||
},
|
||||
Platform.FAN: {
|
||||
"async_decrease_speed",
|
||||
"async_handle_set_preset_mode_service",
|
||||
"async_handle_turn_on_service",
|
||||
"async_increase_speed",
|
||||
"async_oscillate",
|
||||
"async_set_direction",
|
||||
"async_set_percentage",
|
||||
"async_set_preset_mode",
|
||||
"async_toggle",
|
||||
"async_turn_off",
|
||||
"async_turn_on",
|
||||
"decrease_speed",
|
||||
"increase_speed",
|
||||
"oscillate",
|
||||
"set_direction",
|
||||
"set_percentage",
|
||||
"set_preset_mode",
|
||||
"toggle",
|
||||
"turn_off",
|
||||
"turn_on",
|
||||
},
|
||||
Platform.HUMIDIFIER: {
|
||||
"async_service_humidity_set",
|
||||
"async_set_humidity",
|
||||
"async_set_mode",
|
||||
"async_toggle",
|
||||
"async_turn_off",
|
||||
"async_turn_on",
|
||||
"set_humidity",
|
||||
"set_mode",
|
||||
"toggle",
|
||||
"turn_off",
|
||||
"turn_on",
|
||||
},
|
||||
Platform.IMAGE: {
|
||||
"async_image",
|
||||
"image",
|
||||
},
|
||||
Platform.LAWN_MOWER: {
|
||||
"async_dock",
|
||||
"async_pause",
|
||||
"async_start_mowing",
|
||||
"dock",
|
||||
"pause",
|
||||
"start_mowing",
|
||||
},
|
||||
Platform.LIGHT: {
|
||||
"async_toggle",
|
||||
"async_turn_off",
|
||||
"async_turn_on",
|
||||
"toggle",
|
||||
"turn_off",
|
||||
"turn_on",
|
||||
},
|
||||
Platform.LOCK: {
|
||||
"async_lock",
|
||||
"async_open",
|
||||
"async_unlock",
|
||||
"lock",
|
||||
"open",
|
||||
"unlock",
|
||||
},
|
||||
Platform.MEDIA_PLAYER: {
|
||||
"async_browse_media",
|
||||
"async_clear_playlist",
|
||||
"async_join_players",
|
||||
"async_media_next_track",
|
||||
"async_media_pause",
|
||||
"async_media_play_pause",
|
||||
"async_media_play",
|
||||
"async_media_previous_track",
|
||||
"async_media_seek",
|
||||
"async_media_stop",
|
||||
"async_mute_volume",
|
||||
"async_play_media",
|
||||
"async_search_media",
|
||||
"async_select_sound_mode",
|
||||
"async_select_source",
|
||||
"async_set_repeat",
|
||||
"async_set_shuffle",
|
||||
"async_set_volume_level",
|
||||
"async_toggle",
|
||||
"async_turn_off",
|
||||
"async_turn_on",
|
||||
"async_unjoin_player",
|
||||
"async_volume_down",
|
||||
"async_volume_up",
|
||||
"clear_playlist",
|
||||
"join_players",
|
||||
"media_next_track",
|
||||
"media_pause",
|
||||
"media_play",
|
||||
"media_previous_track",
|
||||
"media_seek",
|
||||
"media_stop",
|
||||
"mute_volume",
|
||||
"play_media",
|
||||
"select_sound_mode",
|
||||
"select_source",
|
||||
"set_repeat",
|
||||
"set_shuffle",
|
||||
"set_volume_level",
|
||||
"turn_off",
|
||||
"turn_on",
|
||||
"unjoin_player",
|
||||
},
|
||||
Platform.NOTIFY: {
|
||||
"async_send_message",
|
||||
"send_message",
|
||||
},
|
||||
Platform.NUMBER: {
|
||||
"async_set_native_value",
|
||||
"async_set_value",
|
||||
"set_native_value",
|
||||
"set_value",
|
||||
},
|
||||
Platform.REMOTE: {
|
||||
"async_delete_command",
|
||||
"async_learn_command",
|
||||
"async_send_command",
|
||||
"async_toggle",
|
||||
"async_turn_off",
|
||||
"async_turn_on",
|
||||
"delete_command",
|
||||
"learn_command",
|
||||
"send_command",
|
||||
"toggle",
|
||||
"turn_off",
|
||||
"turn_on",
|
||||
},
|
||||
Platform.SCENE: {
|
||||
"activate",
|
||||
"async_activate",
|
||||
},
|
||||
Platform.SELECT: {
|
||||
"async_select_option",
|
||||
"select_option",
|
||||
},
|
||||
Platform.SIREN: {
|
||||
"async_toggle",
|
||||
"async_turn_off",
|
||||
"async_turn_on",
|
||||
"toggle",
|
||||
"turn_off",
|
||||
"turn_on",
|
||||
},
|
||||
Platform.SWITCH: {
|
||||
"async_toggle",
|
||||
"async_turn_off",
|
||||
"async_turn_on",
|
||||
"toggle",
|
||||
"turn_off",
|
||||
"turn_on",
|
||||
},
|
||||
Platform.TEXT: {
|
||||
"async_set_value",
|
||||
"set_value",
|
||||
},
|
||||
Platform.TIME: {
|
||||
"async_set_value",
|
||||
"set_value",
|
||||
},
|
||||
Platform.TODO: {
|
||||
"async_create_todo_item",
|
||||
"async_delete_todo_items",
|
||||
"async_update_todo_item",
|
||||
},
|
||||
Platform.TTS: {
|
||||
"async_clear_cache",
|
||||
"async_speak",
|
||||
},
|
||||
Platform.UPDATE: {
|
||||
"async_install",
|
||||
"install",
|
||||
},
|
||||
Platform.VACUUM: {
|
||||
"async_clean_segments",
|
||||
"async_clean_spot",
|
||||
"async_locate",
|
||||
"async_pause",
|
||||
"async_return_to_base",
|
||||
"async_send_command",
|
||||
"async_set_fan_speed",
|
||||
"async_start",
|
||||
"async_stop",
|
||||
"clean_segments",
|
||||
"clean_spot",
|
||||
"locate",
|
||||
"pause",
|
||||
"return_to_base",
|
||||
"send_command",
|
||||
"set_fan_speed",
|
||||
"start",
|
||||
"stop",
|
||||
},
|
||||
Platform.VALVE: {
|
||||
"async_close_valve",
|
||||
"async_open_valve",
|
||||
"async_set_valve_position",
|
||||
"async_stop_valve",
|
||||
"async_toggle",
|
||||
"close_valve",
|
||||
"open_valve",
|
||||
"set_valve_position",
|
||||
"stop_valve",
|
||||
"toggle",
|
||||
},
|
||||
Platform.WATER_HEATER: {
|
||||
"async_set_operating_mode",
|
||||
"async_set_temperature",
|
||||
"async_turn_away_mode_off",
|
||||
"async_turn_away_mode_on",
|
||||
"async_turn_off",
|
||||
"async_turn_on",
|
||||
"set_operating_mode",
|
||||
"set_temperature",
|
||||
"turn_away_mode_off",
|
||||
"turn_away_mode_on",
|
||||
"turn_off",
|
||||
"turn_on",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
"""Shared helpers for action/service checkers."""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from astroid import nodes
|
||||
|
||||
from pylint_home_assistant.helpers.module_info import parse_module
|
||||
|
||||
from .const import PLATFORM_ACTION_METHODS
|
||||
|
||||
|
||||
@dataclass
|
||||
class ActionHandlers:
|
||||
"""Action handler names for a module, split by source.
|
||||
|
||||
``platform_methods`` are entity action methods defined by the platform
|
||||
(e.g., ``async_turn_on`` for switch). These must be on an entity class.
|
||||
|
||||
``registered_handlers`` are dynamically registered service handlers
|
||||
(via ``hass.services.async_register`` etc.). These can be standalone
|
||||
functions or methods.
|
||||
"""
|
||||
|
||||
platform_methods: set[str] = field(default_factory=set)
|
||||
registered_handlers: set[str] = field(default_factory=set)
|
||||
|
||||
@property
|
||||
def all_names(self) -> set[str]:
|
||||
"""Return all handler names."""
|
||||
return self.platform_methods | self.registered_handlers
|
||||
|
||||
|
||||
def collect_action_handlers(module: nodes.Module) -> ActionHandlers:
|
||||
"""Collect all action handler names for the given module.
|
||||
|
||||
Returns an ``ActionHandlers`` with platform methods and dynamically
|
||||
registered handlers separated, so the checker can apply the right
|
||||
scope rules to each.
|
||||
"""
|
||||
result = ActionHandlers()
|
||||
|
||||
parsed = parse_module(module.name)
|
||||
if parsed is None:
|
||||
return result
|
||||
|
||||
# Add platform-specific entity action methods
|
||||
if parsed.module and (
|
||||
platform_methods := PLATFORM_ACTION_METHODS.get(parsed.module)
|
||||
):
|
||||
result.platform_methods = set(platform_methods)
|
||||
|
||||
# Discover dynamically registered service handlers
|
||||
for call in module.nodes_of_class(nodes.Call):
|
||||
match call.func:
|
||||
case nodes.Attribute(attrname="async_register_entity_service"):
|
||||
if len(call.args) >= 3:
|
||||
# String method name: "async_set_speed"
|
||||
if isinstance(call.args[2], nodes.Const):
|
||||
result.registered_handlers.add(call.args[2].value)
|
||||
# Function reference: async_handle_snapshot_service
|
||||
elif isinstance(call.args[2], nodes.Name):
|
||||
result.registered_handlers.add(call.args[2].name)
|
||||
# hass.services.async_register(DOMAIN, "name", handler)
|
||||
case nodes.Attribute(
|
||||
attrname="async_register",
|
||||
expr=nodes.Attribute(attrname="services"),
|
||||
):
|
||||
if len(call.args) >= 3 and isinstance(call.args[2], nodes.Name):
|
||||
result.registered_handlers.add(call.args[2].name)
|
||||
# async_register_admin_service(hass, DOMAIN, "name", handler)
|
||||
# Also matches service.async_register_admin_service(...)
|
||||
case (
|
||||
nodes.Name(name="async_register_admin_service")
|
||||
| nodes.Attribute(attrname="async_register_admin_service")
|
||||
):
|
||||
if len(call.args) >= 4 and isinstance(call.args[3], nodes.Name):
|
||||
result.registered_handlers.add(call.args[3].name)
|
||||
|
||||
return result
|
||||
@@ -0,0 +1,249 @@
|
||||
"""Checker for swallowed exceptions in action handlers.
|
||||
|
||||
Service/action handlers in integrations must not silently swallow
|
||||
exceptions. If an action handler catches a library exception and only
|
||||
logs it (or suppresses it via ``contextlib.suppress``), the user gets
|
||||
no feedback in the UI when the action fails.
|
||||
|
||||
This checker detects suppression only — it does not validate *what*
|
||||
exception type is raised. A separate checker should verify that raised
|
||||
exceptions are ``HomeAssistantError`` subclasses with proper translations.
|
||||
|
||||
This rule only applies to modules inside ``homeassistant.components.*``,
|
||||
not to test code or core framework code.
|
||||
"""
|
||||
|
||||
from astroid import nodes
|
||||
from pylint.checkers import BaseChecker
|
||||
from pylint.lint import PyLinter
|
||||
|
||||
from .helpers import ActionHandlers, collect_action_handlers
|
||||
|
||||
|
||||
def _except_block_swallows(handler: nodes.ExceptHandler) -> bool:
|
||||
"""Return True if the except block swallows the exception.
|
||||
|
||||
Flags blocks that:
|
||||
- Are empty (just ``pass``)
|
||||
- Call ``_LOGGER.error/exception/warning`` and nothing else
|
||||
- Log then ``return`` (silently swallowing the error)
|
||||
|
||||
Does NOT flag blocks that:
|
||||
- Contain a ``raise`` statement (any kind)
|
||||
"""
|
||||
has_log_call = False
|
||||
has_any_statement = False
|
||||
for child in handler.body:
|
||||
if isinstance(child, nodes.Raise):
|
||||
return False
|
||||
if isinstance(child, nodes.Pass):
|
||||
continue
|
||||
has_any_statement = True
|
||||
if isinstance(child, nodes.Expr) and isinstance(child.value, nodes.Call):
|
||||
call = child.value
|
||||
if (
|
||||
isinstance(call.func, nodes.Attribute)
|
||||
and call.func.attrname in ("error", "exception", "warning")
|
||||
and isinstance(call.func.expr, nodes.Name)
|
||||
and call.func.expr.name in ("_LOGGER", "LOGGER")
|
||||
):
|
||||
has_log_call = True
|
||||
continue
|
||||
if isinstance(child, nodes.Return) and has_log_call:
|
||||
return True
|
||||
|
||||
# Empty body (just pass) or log-only body
|
||||
return not has_any_statement or has_log_call
|
||||
|
||||
|
||||
def _is_contextlib_suppress(node: nodes.NodeNG) -> bool:
|
||||
"""Return True if *node* is a ``contextlib.suppress(...)`` call.
|
||||
|
||||
Only matches ``contextlib.suppress(...)`` (attribute access form),
|
||||
not a bare ``suppress(...)`` which could be an unrelated function.
|
||||
"""
|
||||
if not isinstance(node, nodes.Call):
|
||||
return False
|
||||
return (
|
||||
isinstance(node.func, nodes.Attribute)
|
||||
and node.func.attrname == "suppress"
|
||||
and isinstance(node.func.expr, nodes.Name)
|
||||
and node.func.expr.name == "contextlib"
|
||||
)
|
||||
|
||||
|
||||
def _is_action_handler(node: nodes.FunctionDef, handlers: ActionHandlers) -> bool:
|
||||
"""Return True if *node* is a registered action handler.
|
||||
|
||||
Platform action methods are scoped by module (the ``ActionHandlers``
|
||||
only contains methods for the current platform), so any method on a
|
||||
class with base classes is accepted. Dynamically registered handlers
|
||||
can be standalone functions or methods on any class.
|
||||
"""
|
||||
if isinstance(node.parent, nodes.ClassDef):
|
||||
# Method on a class — accept if name matches platform or registered
|
||||
# handlers AND the class has base classes (not a plain helper class)
|
||||
if node.name in handlers.platform_methods:
|
||||
return bool(node.parent.bases)
|
||||
return node.name in handlers.registered_handlers
|
||||
# Standalone function — only valid if dynamically registered
|
||||
return node.name in handlers.registered_handlers
|
||||
|
||||
|
||||
def _check_body_shallow(
|
||||
body: list[nodes.NodeNG],
|
||||
) -> nodes.NodeNG | None:
|
||||
"""Check a function body for swallowed exceptions, non-recursively.
|
||||
|
||||
Checks try/except and contextlib.suppress at the current nesting level
|
||||
and inside control flow (if/for/while/with), but does NOT recurse
|
||||
into nested function definitions.
|
||||
|
||||
Returns the first offending node, or None.
|
||||
"""
|
||||
for child in body:
|
||||
if isinstance(child, nodes.Try):
|
||||
for handler in child.handlers:
|
||||
if _except_block_swallows(handler):
|
||||
return handler
|
||||
elif isinstance(child, (nodes.With, nodes.AsyncWith)):
|
||||
# Check the context manager for contextlib.suppress
|
||||
for ctx, _ in child.items:
|
||||
if _is_contextlib_suppress(ctx):
|
||||
return child
|
||||
# Also recurse into the with body for try/except blocks
|
||||
result = _check_body_shallow(child.body)
|
||||
if result:
|
||||
return result
|
||||
elif isinstance(child, nodes.If):
|
||||
result = _check_body_shallow(child.body) or _check_body_shallow(
|
||||
child.orelse
|
||||
)
|
||||
if result:
|
||||
return result
|
||||
elif isinstance(child, (nodes.For, nodes.AsyncFor, nodes.While)):
|
||||
result = _check_body_shallow(child.body)
|
||||
if result:
|
||||
return result
|
||||
return None
|
||||
|
||||
|
||||
class SwallowedActionExceptionsChecker(BaseChecker):
|
||||
"""Checker for swallowed exceptions in service action handlers."""
|
||||
|
||||
name = "home_assistant_actions_swallowed_exceptions"
|
||||
priority = -1
|
||||
msgs = {
|
||||
"E7405": (
|
||||
"Exception in '%s' is swallowed — the error is not raised, "
|
||||
"so the user will not be notified of the failure",
|
||||
"home-assistant-action-swallowed-exception",
|
||||
"Used when a service action handler catches an exception but "
|
||||
"only logs it or suppresses it instead of re-raising. The user "
|
||||
"needs to see the error in the UI.",
|
||||
),
|
||||
}
|
||||
options = ()
|
||||
|
||||
_action_handlers: ActionHandlers
|
||||
|
||||
def visit_module(self, node: nodes.Module) -> None:
|
||||
"""Determine which action handlers to check for this module."""
|
||||
self._action_handlers = collect_action_handlers(node)
|
||||
|
||||
def visit_functiondef(self, node: nodes.FunctionDef) -> None:
|
||||
"""Check action handlers for swallowed exceptions."""
|
||||
if not self._action_handlers.all_names:
|
||||
return
|
||||
|
||||
if not _is_action_handler(node, self._action_handlers):
|
||||
return
|
||||
|
||||
# Check the function body (shallow — no nested function defs)
|
||||
if offending := _check_body_shallow(node.body):
|
||||
self.add_message(
|
||||
"home-assistant-action-swallowed-exception",
|
||||
node=offending,
|
||||
args=(node.name,),
|
||||
)
|
||||
|
||||
# Check decorators — they may wrap the function and swallow exceptions
|
||||
if node.decorators:
|
||||
for decorator in node.decorators.nodes:
|
||||
self._check_decorator(node, decorator)
|
||||
|
||||
visit_asyncfunctiondef = visit_functiondef
|
||||
|
||||
def _check_decorator(
|
||||
self, node: nodes.FunctionDef, decorator: nodes.NodeNG
|
||||
) -> None:
|
||||
"""Check if a decorator swallows exceptions.
|
||||
|
||||
Uses astroid's inference to resolve the decorator function — works
|
||||
across modules (e.g., a decorator imported from ``entity.py``).
|
||||
Only checks decorators defined in ``homeassistant`` code, not
|
||||
stdlib or third-party decorators.
|
||||
"""
|
||||
infer_node = decorator
|
||||
if isinstance(decorator, nodes.Call):
|
||||
infer_node = decorator.func
|
||||
|
||||
try:
|
||||
for inferred in infer_node.infer():
|
||||
if not isinstance(inferred, nodes.FunctionDef):
|
||||
continue
|
||||
# Skip decorators not defined in homeassistant code
|
||||
# (stdlib decorators like @final, @override, @cache have
|
||||
# internal try/except that would cause false positives)
|
||||
module_name = inferred.root().name
|
||||
if not module_name.startswith("homeassistant."):
|
||||
continue
|
||||
if _decorator_swallows(inferred):
|
||||
self.add_message(
|
||||
"home-assistant-action-swallowed-exception",
|
||||
node=decorator,
|
||||
args=(node.name,),
|
||||
)
|
||||
return
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
|
||||
|
||||
def _decorator_swallows(func: nodes.FunctionDef) -> bool:
|
||||
"""Return True if a decorator function swallows exceptions.
|
||||
|
||||
Finds the returned wrapper function and checks its body.
|
||||
"""
|
||||
wrapper = _find_returned_function(func)
|
||||
if wrapper is not None:
|
||||
return _check_body_shallow(wrapper.body) is not None
|
||||
return _check_body_shallow(func.body) is not None
|
||||
|
||||
|
||||
def _find_returned_function(func: nodes.FunctionDef) -> nodes.FunctionDef | None:
|
||||
"""Find the function returned by a decorator or decorator factory."""
|
||||
inner_funcs: dict[str, nodes.FunctionDef] = {}
|
||||
for child in func.body:
|
||||
if isinstance(child, nodes.FunctionDef):
|
||||
inner_funcs[child.name] = child
|
||||
|
||||
if not inner_funcs:
|
||||
return None
|
||||
|
||||
for child in func.body:
|
||||
if isinstance(child, nodes.Return) and isinstance(child.value, nodes.Name):
|
||||
if returned := inner_funcs.get(child.value.name):
|
||||
deeper = _find_returned_function(returned)
|
||||
return deeper if deeper is not None else returned
|
||||
|
||||
if len(inner_funcs) == 1:
|
||||
inner = next(iter(inner_funcs.values()))
|
||||
deeper = _find_returned_function(inner)
|
||||
return deeper if deeper is not None else inner
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def register(linter: PyLinter) -> None:
|
||||
"""Register the checker."""
|
||||
linter.register_checker(SwallowedActionExceptionsChecker(linter))
|
||||
@@ -0,0 +1 @@
|
||||
"""Tests for action/service checkers."""
|
||||
@@ -0,0 +1,667 @@
|
||||
"""Tests for the error propagation checker."""
|
||||
|
||||
import astroid
|
||||
from pylint.testutils import UnittestLinter
|
||||
from pylint.utils.ast_walker import ASTWalker
|
||||
from pylint_home_assistant.checkers.actions.swallowed_exceptions import (
|
||||
SwallowedActionExceptionsChecker,
|
||||
)
|
||||
import pytest
|
||||
|
||||
from tests.pylint import assert_no_messages
|
||||
|
||||
|
||||
@pytest.fixture(name="error_propagation_checker")
|
||||
def error_propagation_checker_fixture(
|
||||
linter: UnittestLinter,
|
||||
) -> SwallowedActionExceptionsChecker:
|
||||
"""Fixture to provide an error propagation checker."""
|
||||
return SwallowedActionExceptionsChecker(linter)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"code",
|
||||
[
|
||||
pytest.param(
|
||||
"""
|
||||
class MySwitch(SwitchEntity):
|
||||
async def async_turn_on(self, **kwargs) -> None:
|
||||
try:
|
||||
await self.device.turn_on()
|
||||
except DeviceError as err:
|
||||
raise HomeAssistantError("Failed") from err
|
||||
""",
|
||||
id="raises_homeassistant_error",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
class MySwitch(SwitchEntity):
|
||||
async def async_turn_on(self, **kwargs) -> None:
|
||||
try:
|
||||
await self.device.turn_on()
|
||||
except DeviceError as err:
|
||||
raise ServiceValidationError("Invalid") from err
|
||||
""",
|
||||
id="raises_service_validation_error",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
class MySwitch(SwitchEntity):
|
||||
async def async_turn_on(self, **kwargs) -> None:
|
||||
await self.device.turn_on()
|
||||
""",
|
||||
id="no_try_except_at_all",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
class MySwitch(SwitchEntity):
|
||||
async def async_turn_on(self, **kwargs) -> None:
|
||||
try:
|
||||
await self.device.turn_on()
|
||||
except DeviceError:
|
||||
raise
|
||||
""",
|
||||
id="bare_raise",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
class MySwitch(SwitchEntity):
|
||||
def helper_method(self) -> None:
|
||||
try:
|
||||
self.device.do_something()
|
||||
except DeviceError:
|
||||
_LOGGER.error("Failed")
|
||||
""",
|
||||
id="non_action_method_ignored",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
class MySwitch(SwitchEntity):
|
||||
async def async_turn_on(self, **kwargs) -> None:
|
||||
try:
|
||||
await self.device.turn_on()
|
||||
except DeviceError:
|
||||
_LOGGER.error("Failed")
|
||||
raise HomeAssistantError("Failed")
|
||||
""",
|
||||
id="logs_and_raises",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
class ApiClient:
|
||||
async def async_turn_on(self) -> None:
|
||||
try:
|
||||
await self.api.enable()
|
||||
except ApiError:
|
||||
_LOGGER.error("Failed")
|
||||
""",
|
||||
id="non_entity_class_no_bases_ignored",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
async def async_turn_on():
|
||||
try:
|
||||
await api.enable()
|
||||
except ApiError:
|
||||
_LOGGER.error("Failed")
|
||||
""",
|
||||
id="standalone_function_matching_platform_name_ignored",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
class MySwitch(SwitchEntity):
|
||||
async def async_turn_on(self, **kwargs) -> None:
|
||||
def _callback():
|
||||
try:
|
||||
do_something()
|
||||
except DeviceError:
|
||||
_LOGGER.error("Nested failure")
|
||||
await self.device.turn_on(callback=_callback)
|
||||
""",
|
||||
id="nested_function_not_flagged",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_no_warning(
|
||||
linter: UnittestLinter,
|
||||
error_propagation_checker: SwallowedActionExceptionsChecker,
|
||||
code: str,
|
||||
) -> None:
|
||||
"""Test cases that should not trigger a warning."""
|
||||
root_node = astroid.parse(code, "homeassistant.components.test_integration.switch")
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(error_propagation_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_log_only_flagged(
|
||||
linter: UnittestLinter,
|
||||
error_propagation_checker: SwallowedActionExceptionsChecker,
|
||||
) -> None:
|
||||
"""Test that logging without raising is flagged."""
|
||||
root_node = astroid.parse(
|
||||
"""
|
||||
class MySwitch(SwitchEntity):
|
||||
async def async_turn_on(self, **kwargs) -> None:
|
||||
try:
|
||||
await self.device.turn_on()
|
||||
except DeviceError:
|
||||
_LOGGER.error("Failed to turn on")
|
||||
""",
|
||||
"homeassistant.components.test_integration.switch",
|
||||
)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(error_propagation_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
assert messages[0].msg_id == "home-assistant-action-swallowed-exception"
|
||||
assert messages[0].args == ("async_turn_on",)
|
||||
|
||||
|
||||
def test_log_and_return_flagged(
|
||||
linter: UnittestLinter,
|
||||
error_propagation_checker: SwallowedActionExceptionsChecker,
|
||||
) -> None:
|
||||
"""Test that logging then returning is flagged."""
|
||||
root_node = astroid.parse(
|
||||
"""
|
||||
class MySwitch(SwitchEntity):
|
||||
async def async_turn_on(self, **kwargs) -> None:
|
||||
try:
|
||||
await self.device.turn_on()
|
||||
except DeviceError as error:
|
||||
_LOGGER.error(error)
|
||||
return
|
||||
""",
|
||||
"homeassistant.components.test_integration.switch",
|
||||
)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(error_propagation_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
|
||||
|
||||
def test_except_pass_flagged(
|
||||
linter: UnittestLinter,
|
||||
error_propagation_checker: SwallowedActionExceptionsChecker,
|
||||
) -> None:
|
||||
"""Test that except with only pass is flagged."""
|
||||
root_node = astroid.parse(
|
||||
"""
|
||||
class MySwitch(SwitchEntity):
|
||||
async def async_turn_on(self, **kwargs) -> None:
|
||||
try:
|
||||
await self.device.turn_on()
|
||||
except DeviceError:
|
||||
pass
|
||||
""",
|
||||
"homeassistant.components.test_integration.switch",
|
||||
)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(error_propagation_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
assert messages[0].args == ("async_turn_on",)
|
||||
|
||||
|
||||
def test_try_except_inside_with_flagged(
|
||||
linter: UnittestLinter,
|
||||
error_propagation_checker: SwallowedActionExceptionsChecker,
|
||||
) -> None:
|
||||
"""Test that try/except inside a with block is caught."""
|
||||
root_node = astroid.parse(
|
||||
"""
|
||||
class MySwitch(SwitchEntity):
|
||||
async def async_turn_on(self, **kwargs) -> None:
|
||||
with some_context():
|
||||
try:
|
||||
await self.device.turn_on()
|
||||
except DeviceError:
|
||||
_LOGGER.error("Failed")
|
||||
""",
|
||||
"homeassistant.components.test_integration.switch",
|
||||
)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(error_propagation_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
assert messages[0].args == ("async_turn_on",)
|
||||
|
||||
|
||||
def test_try_except_inside_async_with_flagged(
|
||||
linter: UnittestLinter,
|
||||
error_propagation_checker: SwallowedActionExceptionsChecker,
|
||||
) -> None:
|
||||
"""Test that try/except inside an async with block is caught."""
|
||||
root_node = astroid.parse(
|
||||
"""
|
||||
class MySwitch(SwitchEntity):
|
||||
async def async_turn_on(self, **kwargs) -> None:
|
||||
async with some_context():
|
||||
try:
|
||||
await self.device.turn_on()
|
||||
except DeviceError:
|
||||
_LOGGER.error("Failed")
|
||||
""",
|
||||
"homeassistant.components.test_integration.switch",
|
||||
)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(error_propagation_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
|
||||
|
||||
def test_try_except_inside_async_for_flagged(
|
||||
linter: UnittestLinter,
|
||||
error_propagation_checker: SwallowedActionExceptionsChecker,
|
||||
) -> None:
|
||||
"""Test that try/except inside an async for block is caught."""
|
||||
root_node = astroid.parse(
|
||||
"""
|
||||
class MySwitch(SwitchEntity):
|
||||
async def async_turn_on(self, **kwargs) -> None:
|
||||
async for item in some_iterable():
|
||||
try:
|
||||
await self.device.turn_on(item)
|
||||
except DeviceError:
|
||||
_LOGGER.error("Failed")
|
||||
""",
|
||||
"homeassistant.components.test_integration.switch",
|
||||
)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(error_propagation_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
|
||||
|
||||
def test_exception_handler_flagged(
|
||||
linter: UnittestLinter,
|
||||
error_propagation_checker: SwallowedActionExceptionsChecker,
|
||||
) -> None:
|
||||
"""Test that _LOGGER.exception without raising is flagged."""
|
||||
root_node = astroid.parse(
|
||||
"""
|
||||
class MySwitch(SwitchEntity):
|
||||
async def async_turn_on(self, **kwargs) -> None:
|
||||
try:
|
||||
await self.device.turn_on()
|
||||
except DeviceError:
|
||||
_LOGGER.exception("Failed to turn on")
|
||||
""",
|
||||
"homeassistant.components.test_integration.switch",
|
||||
)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(error_propagation_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
|
||||
|
||||
def test_multiple_action_methods(
|
||||
linter: UnittestLinter,
|
||||
error_propagation_checker: SwallowedActionExceptionsChecker,
|
||||
) -> None:
|
||||
"""Test that multiple bad methods are each flagged."""
|
||||
root_node = astroid.parse(
|
||||
"""
|
||||
class MySwitch(SwitchEntity):
|
||||
async def async_turn_on(self, **kwargs) -> None:
|
||||
try:
|
||||
await self.device.turn_on()
|
||||
except DeviceError:
|
||||
_LOGGER.error("Failed")
|
||||
|
||||
async def async_turn_off(self, **kwargs) -> None:
|
||||
try:
|
||||
await self.device.turn_off()
|
||||
except DeviceError:
|
||||
_LOGGER.error("Failed")
|
||||
""",
|
||||
"homeassistant.components.test_integration.switch",
|
||||
)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(error_propagation_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 2
|
||||
|
||||
|
||||
def test_contextlib_suppress_flagged(
|
||||
linter: UnittestLinter,
|
||||
error_propagation_checker: SwallowedActionExceptionsChecker,
|
||||
) -> None:
|
||||
"""Test that contextlib.suppress in action methods is flagged."""
|
||||
root_node = astroid.parse(
|
||||
"""
|
||||
class MySwitch(SwitchEntity):
|
||||
async def async_turn_on(self, **kwargs) -> None:
|
||||
with contextlib.suppress(DeviceError):
|
||||
await self.device.turn_on()
|
||||
""",
|
||||
"homeassistant.components.test_integration.switch",
|
||||
)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(error_propagation_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
assert messages[0].args == ("async_turn_on",)
|
||||
|
||||
|
||||
def test_bare_suppress_not_flagged(
|
||||
linter: UnittestLinter,
|
||||
error_propagation_checker: SwallowedActionExceptionsChecker,
|
||||
) -> None:
|
||||
"""Test that bare suppress() (not contextlib.suppress) is not flagged."""
|
||||
root_node = astroid.parse(
|
||||
"""
|
||||
class MySwitch(SwitchEntity):
|
||||
async def async_turn_off(self, **kwargs) -> None:
|
||||
with suppress(DeviceError):
|
||||
await self.device.turn_off()
|
||||
""",
|
||||
"homeassistant.components.test_integration.switch",
|
||||
)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(error_propagation_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_decorator_swallows_flagged(
|
||||
linter: UnittestLinter,
|
||||
error_propagation_checker: SwallowedActionExceptionsChecker,
|
||||
) -> None:
|
||||
"""Test that decorators that swallow exceptions are flagged."""
|
||||
root_node = astroid.parse(
|
||||
"""
|
||||
def my_error_handler(func):
|
||||
async def wrapper(self, *args, **kwargs):
|
||||
try:
|
||||
return await func(self, *args, **kwargs)
|
||||
except DeviceError:
|
||||
_LOGGER.error("Device error")
|
||||
return wrapper
|
||||
|
||||
class MySwitch(SwitchEntity):
|
||||
@my_error_handler
|
||||
async def async_turn_on(self, **kwargs) -> None:
|
||||
await self.device.turn_on()
|
||||
""",
|
||||
"homeassistant.components.test_integration.switch",
|
||||
)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(error_propagation_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
assert messages[0].args == ("async_turn_on",)
|
||||
|
||||
|
||||
def test_decorator_factory_swallows_flagged(
|
||||
linter: UnittestLinter,
|
||||
error_propagation_checker: SwallowedActionExceptionsChecker,
|
||||
) -> None:
|
||||
"""Test that decorator factories that swallow exceptions are flagged."""
|
||||
root_node = astroid.parse(
|
||||
"""
|
||||
def my_error_handler(override=False):
|
||||
def _decorator(func):
|
||||
async def wrapper(self, *args, **kwargs):
|
||||
try:
|
||||
return await func(self, *args, **kwargs)
|
||||
except DeviceError:
|
||||
_LOGGER.error("Device error")
|
||||
return wrapper
|
||||
return _decorator
|
||||
|
||||
class MySwitch(SwitchEntity):
|
||||
@my_error_handler(override=True)
|
||||
async def async_turn_on(self, **kwargs) -> None:
|
||||
await self.device.turn_on()
|
||||
""",
|
||||
"homeassistant.components.test_integration.switch",
|
||||
)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(error_propagation_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
|
||||
|
||||
def test_decorator_raises_ha_error_ok(
|
||||
linter: UnittestLinter,
|
||||
error_propagation_checker: SwallowedActionExceptionsChecker,
|
||||
) -> None:
|
||||
"""Test that decorators that properly raise HomeAssistantError pass."""
|
||||
root_node = astroid.parse(
|
||||
"""
|
||||
def convert_error(func):
|
||||
async def wrapper(self, *args, **kwargs):
|
||||
try:
|
||||
return await func(self, *args, **kwargs)
|
||||
except DeviceError as err:
|
||||
raise HomeAssistantError("Failed") from err
|
||||
return wrapper
|
||||
|
||||
class MySwitch(SwitchEntity):
|
||||
@convert_error
|
||||
async def async_turn_on(self, **kwargs) -> None:
|
||||
await self.device.turn_on()
|
||||
""",
|
||||
"homeassistant.components.test_integration.switch",
|
||||
)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(error_propagation_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_custom_service_method_flagged(
|
||||
linter: UnittestLinter,
|
||||
error_propagation_checker: SwallowedActionExceptionsChecker,
|
||||
) -> None:
|
||||
"""Test that custom registered service methods are also checked."""
|
||||
root_node = astroid.parse(
|
||||
"""
|
||||
async def async_setup_entry(hass, entry, async_add_entities):
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
platform.async_register_entity_service(
|
||||
"set_speed",
|
||||
{vol.Required("speed"): cv.string},
|
||||
"async_set_speed",
|
||||
)
|
||||
|
||||
class MyFan(FanEntity):
|
||||
async def async_set_speed(self, speed: str) -> None:
|
||||
try:
|
||||
await self.device.set_speed(speed)
|
||||
except DeviceError:
|
||||
_LOGGER.error("Failed to set speed")
|
||||
""",
|
||||
"homeassistant.components.test_integration.fan",
|
||||
)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(error_propagation_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
assert messages[0].args == ("async_set_speed",)
|
||||
|
||||
|
||||
def test_custom_service_method_good(
|
||||
linter: UnittestLinter,
|
||||
error_propagation_checker: SwallowedActionExceptionsChecker,
|
||||
) -> None:
|
||||
"""Test that custom service methods with proper error handling pass."""
|
||||
root_node = astroid.parse(
|
||||
"""
|
||||
async def async_setup_entry(hass, entry, async_add_entities):
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
platform.async_register_entity_service(
|
||||
"set_speed",
|
||||
{vol.Required("speed"): cv.string},
|
||||
"async_set_speed",
|
||||
)
|
||||
|
||||
class MyFan(FanEntity):
|
||||
async def async_set_speed(self, speed: str) -> None:
|
||||
try:
|
||||
await self.device.set_speed(speed)
|
||||
except DeviceError as err:
|
||||
raise HomeAssistantError("Failed") from err
|
||||
""",
|
||||
"homeassistant.components.test_integration.fan",
|
||||
)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(error_propagation_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_unregistered_custom_method_ignored(
|
||||
linter: UnittestLinter,
|
||||
error_propagation_checker: SwallowedActionExceptionsChecker,
|
||||
) -> None:
|
||||
"""Test that random methods not registered as services are ignored."""
|
||||
root_node = astroid.parse(
|
||||
"""
|
||||
class MyFan(FanEntity):
|
||||
async def async_do_something(self) -> None:
|
||||
try:
|
||||
await self.device.do_something()
|
||||
except DeviceError:
|
||||
_LOGGER.error("Failed")
|
||||
""",
|
||||
"homeassistant.components.test_integration.fan",
|
||||
)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(error_propagation_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_standalone_service_handler_flagged(
|
||||
linter: UnittestLinter,
|
||||
error_propagation_checker: SwallowedActionExceptionsChecker,
|
||||
) -> None:
|
||||
"""Test that standalone service handlers registered via hass.services are checked."""
|
||||
root_node = astroid.parse(
|
||||
"""
|
||||
async def async_setup(hass, config):
|
||||
hass.services.async_register(DOMAIN, "do_thing", _handle_do_thing)
|
||||
|
||||
async def _handle_do_thing(call):
|
||||
try:
|
||||
await some_api.do_thing(call.data["target"])
|
||||
except ApiError:
|
||||
_LOGGER.error("Failed to do thing")
|
||||
""",
|
||||
"homeassistant.components.test_integration.services",
|
||||
)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(error_propagation_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
assert messages[0].args == ("_handle_do_thing",)
|
||||
|
||||
|
||||
def test_admin_service_handler_flagged(
|
||||
linter: UnittestLinter,
|
||||
error_propagation_checker: SwallowedActionExceptionsChecker,
|
||||
) -> None:
|
||||
"""Test that admin service handlers are also checked."""
|
||||
root_node = astroid.parse(
|
||||
"""
|
||||
async def async_setup(hass, config):
|
||||
async_register_admin_service(hass, DOMAIN, "reset", _handle_reset)
|
||||
|
||||
async def _handle_reset(call):
|
||||
try:
|
||||
await some_api.reset()
|
||||
except ApiError:
|
||||
_LOGGER.error("Failed to reset")
|
||||
""",
|
||||
"homeassistant.components.test_integration.services",
|
||||
)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(error_propagation_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
assert messages[0].args == ("_handle_reset",)
|
||||
|
||||
|
||||
def test_standalone_service_handler_good(
|
||||
linter: UnittestLinter,
|
||||
error_propagation_checker: SwallowedActionExceptionsChecker,
|
||||
) -> None:
|
||||
"""Test that standalone handlers with proper error propagation pass."""
|
||||
root_node = astroid.parse(
|
||||
"""
|
||||
async def async_setup(hass, config):
|
||||
hass.services.async_register(DOMAIN, "do_thing", _handle_do_thing)
|
||||
|
||||
async def _handle_do_thing(call):
|
||||
try:
|
||||
await some_api.do_thing(call.data["target"])
|
||||
except ApiError as err:
|
||||
raise HomeAssistantError("Failed") from err
|
||||
""",
|
||||
"homeassistant.components.test_integration.services",
|
||||
)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(error_propagation_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_not_integration_module_ignored(
|
||||
linter: UnittestLinter,
|
||||
error_propagation_checker: SwallowedActionExceptionsChecker,
|
||||
) -> None:
|
||||
"""Test that non-integration modules are ignored."""
|
||||
root_node = astroid.parse(
|
||||
"""
|
||||
class MySwitch(SwitchEntity):
|
||||
async def async_turn_on(self, **kwargs) -> None:
|
||||
try:
|
||||
await self.device.turn_on()
|
||||
except DeviceError:
|
||||
_LOGGER.error("Failed")
|
||||
""",
|
||||
"tests.components.test_integration.test_switch",
|
||||
)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(error_propagation_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walker.walk(root_node)
|
||||
Reference in New Issue
Block a user