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:
Franck Nijhof
2026-05-14 20:59:17 +02:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 02666f8762
commit 017f85243a
97 changed files with 1509 additions and 0 deletions
@@ -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)
+1
View File
@@ -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")
+1
View File
@@ -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:
+1
View File
@@ -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'",
+1
View File
@@ -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?")
+1
View File
@@ -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:
+3
View File
@@ -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
+1
View File
@@ -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)
+2
View File
@@ -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")
+2
View File
@@ -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
+6
View File
@@ -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",
+1
View File
@@ -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:
+2
View File
@@ -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:
+1
View File
@@ -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
+1
View File
@@ -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
+2
View File
@@ -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."""
+4
View File
@@ -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."""
+1
View File
@@ -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))
+1
View File
@@ -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)