From 017f85243adb087d47760a268b4ae0f93e094162 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 14 May 2026 20:59:17 +0200 Subject: [PATCH] Add pylint checker for swallowed exceptions in action handlers (#170652) Co-authored-by: Claude Opus 4.6 (1M context) --- homeassistant/components/abode/services.py | 1 + homeassistant/components/adguard/switch.py | 2 + homeassistant/components/airobot/button.py | 1 + .../components/arcam_fmj/media_player.py | 1 + .../components/azure_service_bus/notify.py | 1 + homeassistant/components/broadlink/remote.py | 2 + homeassistant/components/cast/media_player.py | 1 + .../components/cisco_webex_teams/notify.py | 1 + .../components/color_extractor/services.py | 1 + .../components/command_line/notify.py | 1 + homeassistant/components/deconz/services.py | 1 + homeassistant/components/decora_wifi/light.py | 2 + .../components/denonavr/media_player.py | 14 + homeassistant/components/directv/remote.py | 1 + homeassistant/components/discord/notify.py | 1 + .../components/dlna_dmr/media_player.py | 12 + homeassistant/components/doorbird/camera.py | 1 + .../components/egardia/alarm_control_panel.py | 3 + .../components/enigma2/media_player.py | 1 + .../components/eq3btsmart/climate.py | 2 + homeassistant/components/flock/notify.py | 1 + homeassistant/components/generic/camera.py | 1 + .../components/homeassistant/__init__.py | 1 + .../components/homeassistant/scene.py | 1 + .../components/horizon/media_player.py | 1 + homeassistant/components/huawei_lte/notify.py | 1 + homeassistant/components/ifttt/__init__.py | 1 + homeassistant/components/kiwi/lock.py | 1 + homeassistant/components/knx/services.py | 1 + homeassistant/components/kodi/notify.py | 1 + homeassistant/components/lg_thinq/fan.py | 1 + homeassistant/components/lifx_cloud/scene.py | 1 + homeassistant/components/local_file/camera.py | 1 + homeassistant/components/lovelace/__init__.py | 1 + homeassistant/components/lyric/climate.py | 5 + homeassistant/components/madvr/remote.py | 3 + homeassistant/components/mailgun/notify.py | 1 + .../components/message_bird/notify.py | 1 + .../components/microsoft_face/__init__.py | 6 + homeassistant/components/mjpeg/camera.py | 1 + homeassistant/components/mochad/light.py | 2 + homeassistant/components/mochad/switch.py | 2 + homeassistant/components/modern_forms/fan.py | 6 + .../components/modern_forms/light.py | 4 + .../components/modern_forms/switch.py | 4 + homeassistant/components/msteams/notify.py | 1 + homeassistant/components/mystrom/light.py | 2 + homeassistant/components/mystrom/switch.py | 2 + homeassistant/components/neato/switch.py | 2 + homeassistant/components/neato/vacuum.py | 6 + homeassistant/components/netatmo/__init__.py | 1 + .../components/netgear_lte/notify.py | 1 + .../components/nfandroidtv/notify.py | 1 + homeassistant/components/numato/switch.py | 2 + homeassistant/components/onvif/camera.py | 1 + .../components/opendisplay/services.py | 1 + .../components/openhome/media_player.py | 13 + homeassistant/components/pi_hole/switch.py | 2 + homeassistant/components/pilight/__init__.py | 1 + homeassistant/components/prosegur/camera.py | 2 + homeassistant/components/pushover/notify.py | 1 + homeassistant/components/qvr_pro/camera.py | 1 + homeassistant/components/rest/__init__.py | 1 + homeassistant/components/rest/switch.py | 2 + homeassistant/components/roomba/vacuum.py | 1 + .../components/samsungtv/media_player.py | 1 + homeassistant/components/schluter/climate.py | 1 + .../components/shell_command/__init__.py | 1 + .../components/shopping_list/__init__.py | 3 + homeassistant/components/simplepush/notify.py | 1 + homeassistant/components/sinch/notify.py | 1 + homeassistant/components/sky_remote/remote.py | 1 + homeassistant/components/slack/notify.py | 1 + homeassistant/components/smlight/light.py | 1 + homeassistant/components/smlight/update.py | 1 + .../components/synology_dsm/services.py | 1 + homeassistant/components/template/__init__.py | 1 + homeassistant/components/toon/climate.py | 2 + homeassistant/components/toon/switch.py | 4 + homeassistant/components/tractive/switch.py | 2 + .../components/twilio_call/notify.py | 1 + .../components/unifi_access/image.py | 1 + homeassistant/components/vallox/services.py | 1 + homeassistant/components/velbus/services.py | 1 + .../components/vlc_telnet/media_player.py | 11 + homeassistant/components/xeoma/camera.py | 1 + homeassistant/components/xiaomi/camera.py | 1 + .../components/xiaomi_miio/vacuum.py | 1 + .../components/yamaha/media_player.py | 1 + .../yamaha_musiccast/media_player.py | 2 + homeassistant/components/yeelight/light.py | 1 + .../checkers/actions/__init__.py | 1 + .../checkers/actions/const.py | 325 +++++++++ .../checkers/actions/helpers.py | 79 +++ .../checkers/actions/swallowed_exceptions.py | 249 +++++++ tests/pylint/actions/__init__.py | 1 + .../actions/test_swallowed_exceptions.py | 667 ++++++++++++++++++ 97 files changed, 1509 insertions(+) create mode 100644 pylint/plugins/pylint_home_assistant/checkers/actions/__init__.py create mode 100644 pylint/plugins/pylint_home_assistant/checkers/actions/const.py create mode 100644 pylint/plugins/pylint_home_assistant/checkers/actions/helpers.py create mode 100644 pylint/plugins/pylint_home_assistant/checkers/actions/swallowed_exceptions.py create mode 100644 tests/pylint/actions/__init__.py create mode 100644 tests/pylint/actions/test_swallowed_exceptions.py diff --git a/homeassistant/components/abode/services.py b/homeassistant/components/abode/services.py index 06cbe3cdffda..2c25acceb08b 100644 --- a/homeassistant/components/abode/services.py +++ b/homeassistant/components/abode/services.py @@ -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) diff --git a/homeassistant/components/adguard/switch.py b/homeassistant/components/adguard/switch.py index 0b952f0f5d65..c94b5cc451f7 100644 --- a/homeassistant/components/adguard/switch.py +++ b/homeassistant/components/adguard/switch.py @@ -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 diff --git a/homeassistant/components/airobot/button.py b/homeassistant/components/airobot/button.py index a4c063a16f11..e54d7292b13f 100644 --- a/homeassistant/components/airobot/button.py +++ b/homeassistant/components/airobot/button.py @@ -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 diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index 4c63aaf1b6b2..ffb7f0dd7bd6 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -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 diff --git a/homeassistant/components/azure_service_bus/notify.py b/homeassistant/components/azure_service_bus/notify.py index 054fa8eeef10..4223901ab2df 100644 --- a/homeassistant/components/azure_service_bus/notify.py +++ b/homeassistant/components/azure_service_bus/notify.py @@ -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", diff --git a/homeassistant/components/broadlink/remote.py b/homeassistant/components/broadlink/remote.py index a6a21c38154a..91dcb487d681 100644 --- a/homeassistant/components/broadlink/remote.py +++ b/homeassistant/components/broadlink/remote.py @@ -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 diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 4fd69f9d8bbb..64aec5d7ed39 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -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 diff --git a/homeassistant/components/cisco_webex_teams/notify.py b/homeassistant/components/cisco_webex_teams/notify.py index 8b68626ad0eb..5b64aad03840 100644 --- a/homeassistant/components/cisco_webex_teams/notify.py +++ b/homeassistant/components/cisco_webex_teams/notify.py @@ -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 diff --git a/homeassistant/components/color_extractor/services.py b/homeassistant/components/color_extractor/services.py index d5d90bca3087..7237a32a3603 100644 --- a/homeassistant/components/color_extractor/services.py +++ b/homeassistant/components/color_extractor/services.py @@ -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", diff --git a/homeassistant/components/command_line/notify.py b/homeassistant/components/command_line/notify.py index e63046a1c837..2f30dde039b2 100644 --- a/homeassistant/components/command_line/notify.py +++ b/homeassistant/components/command_line/notify.py @@ -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) diff --git a/homeassistant/components/deconz/services.py b/homeassistant/components/deconz/services.py index 95f81f9a8a75..bff21a0c69cb 100644 --- a/homeassistant/components/deconz/services.py +++ b/homeassistant/components/deconz/services.py @@ -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 diff --git a/homeassistant/components/decora_wifi/light.py b/homeassistant/components/decora_wifi/light.py index 0fe07caba980..0934dc10a95e 100644 --- a/homeassistant/components/decora_wifi/light.py +++ b/homeassistant/components/decora_wifi/light.py @@ -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") diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index 1077699520bd..c104da3d8c30 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -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.""" diff --git a/homeassistant/components/directv/remote.py b/homeassistant/components/directv/remote.py index fd98a208c808..9ea9f0da849d 100644 --- a/homeassistant/components/directv/remote.py +++ b/homeassistant/components/directv/remote.py @@ -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", diff --git a/homeassistant/components/discord/notify.py b/homeassistant/components/discord/notify.py index cb8900c9c3b8..536249dbabbb 100644 --- a/homeassistant/components/discord/notify.py +++ b/homeassistant/components/discord/notify.py @@ -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() diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index 65d62c047832..4bd503610e79 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -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.""" diff --git a/homeassistant/components/doorbird/camera.py b/homeassistant/components/doorbird/camera.py index 5e1eeb0b0b8a..86ca345d41e1 100644 --- a/homeassistant/components/doorbird/camera.py +++ b/homeassistant/components/doorbird/camera.py @@ -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 diff --git a/homeassistant/components/egardia/alarm_control_panel.py b/homeassistant/components/egardia/alarm_control_panel.py index 2951ad3d841c..acaf100fd911 100644 --- a/homeassistant/components/egardia/alarm_control_panel.py +++ b/homeassistant/components/egardia/alarm_control_panel.py @@ -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", diff --git a/homeassistant/components/enigma2/media_player.py b/homeassistant/components/enigma2/media_player.py index 26a42929af6a..476379423e56 100644 --- a/homeassistant/components/enigma2/media_player.py +++ b/homeassistant/components/enigma2/media_player.py @@ -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 diff --git a/homeassistant/components/eq3btsmart/climate.py b/homeassistant/components/eq3btsmart/climate.py index c11328c7ec3e..0943bc9b19d4 100644 --- a/homeassistant/components/eq3btsmart/climate.py +++ b/homeassistant/components/eq3btsmart/climate.py @@ -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) diff --git a/homeassistant/components/flock/notify.py b/homeassistant/components/flock/notify.py index 5fcf73bddb9b..ce8866492999 100644 --- a/homeassistant/components/flock/notify.py +++ b/homeassistant/components/flock/notify.py @@ -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) diff --git a/homeassistant/components/generic/camera.py b/homeassistant/components/generic/camera.py index 47675ae173b0..fa02d512fa83 100644 --- a/homeassistant/components/generic/camera.py +++ b/homeassistant/components/generic/camera.py @@ -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 diff --git a/homeassistant/components/homeassistant/__init__.py b/homeassistant/components/homeassistant/__init__.py index fdc5376174df..88ba05535a1c 100644 --- a/homeassistant/components/homeassistant/__init__.py +++ b/homeassistant/components/homeassistant/__init__.py @@ -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 diff --git a/homeassistant/components/homeassistant/scene.py b/homeassistant/components/homeassistant/scene.py index c294276f0a1d..89f02bcf7c29 100644 --- a/homeassistant/components/homeassistant/scene.py +++ b/homeassistant/components/homeassistant/scene.py @@ -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 diff --git a/homeassistant/components/horizon/media_player.py b/homeassistant/components/horizon/media_player.py index 5ce6aa404af8..269fc54dd617 100644 --- a/homeassistant/components/horizon/media_player.py +++ b/homeassistant/components/horizon/media_player.py @@ -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: diff --git a/homeassistant/components/huawei_lte/notify.py b/homeassistant/components/huawei_lte/notify.py index ad94799bb16a..29740b5e6646 100644 --- a/homeassistant/components/huawei_lte/notify.py +++ b/homeassistant/components/huawei_lte/notify.py @@ -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) diff --git a/homeassistant/components/ifttt/__init__.py b/homeassistant/components/ifttt/__init__.py index 23820438a5e9..607ce2195593 100644 --- a/homeassistant/components/ifttt/__init__.py +++ b/homeassistant/components/ifttt/__init__.py @@ -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") diff --git a/homeassistant/components/kiwi/lock.py b/homeassistant/components/kiwi/lock.py index 77e0016b8ec2..0091fd1cd341 100644 --- a/homeassistant/components/kiwi/lock.py +++ b/homeassistant/components/kiwi/lock.py @@ -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: diff --git a/homeassistant/components/knx/services.py b/homeassistant/components/knx/services.py index 48b0cd2b99eb..6def16df68a6 100644 --- a/homeassistant/components/knx/services.py +++ b/homeassistant/components/knx/services.py @@ -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'", diff --git a/homeassistant/components/kodi/notify.py b/homeassistant/components/kodi/notify.py index c688b051b81f..7a86f6ebf2ba 100644 --- a/homeassistant/components/kodi/notify.py +++ b/homeassistant/components/kodi/notify.py @@ -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?") diff --git a/homeassistant/components/lg_thinq/fan.py b/homeassistant/components/lg_thinq/fan.py index 35ad9ddeb73d..c27322bda831 100644 --- a/homeassistant/components/lg_thinq/fan.py +++ b/homeassistant/components/lg_thinq/fan.py @@ -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 diff --git a/homeassistant/components/lifx_cloud/scene.py b/homeassistant/components/lifx_cloud/scene.py index cdeaaccace5a..20f7db824c5e 100644 --- a/homeassistant/components/lifx_cloud/scene.py +++ b/homeassistant/components/lifx_cloud/scene.py @@ -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) diff --git a/homeassistant/components/local_file/camera.py b/homeassistant/components/local_file/camera.py index 3e225125dda6..7afbeb684e65 100644 --- a/homeassistant/components/local_file/camera.py +++ b/homeassistant/components/local_file/camera.py @@ -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", diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index dae507ca768c..2431d736f67d 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -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 diff --git a/homeassistant/components/lyric/climate.py b/homeassistant/components/lyric/climate.py index cb274efa54af..3ffa6a6c23be 100644 --- a/homeassistant/components/lyric/climate.py +++ b/homeassistant/components/lyric/climate.py @@ -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: diff --git a/homeassistant/components/madvr/remote.py b/homeassistant/components/madvr/remote.py index ec753ed384ec..25f572249877 100644 --- a/homeassistant/components/madvr/remote.py +++ b/homeassistant/components/madvr/remote.py @@ -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) diff --git a/homeassistant/components/mailgun/notify.py b/homeassistant/components/mailgun/notify.py index 2b98710f1f3a..8bc60bb5522a 100644 --- a/homeassistant/components/mailgun/notify.py +++ b/homeassistant/components/mailgun/notify.py @@ -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") diff --git a/homeassistant/components/message_bird/notify.py b/homeassistant/components/message_bird/notify.py index 95d4ee79fb7b..841b9fb44f5a 100644 --- a/homeassistant/components/message_bird/notify.py +++ b/homeassistant/components/message_bird/notify.py @@ -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 diff --git a/homeassistant/components/microsoft_face/__init__.py b/homeassistant/components/microsoft_face/__init__.py index 443e934a3180..6b4adb6ffc6c 100644 --- a/homeassistant/components/microsoft_face/__init__.py +++ b/homeassistant/components/microsoft_face/__init__.py @@ -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 diff --git a/homeassistant/components/mjpeg/camera.py b/homeassistant/components/mjpeg/camera.py index 5a1c32c22604..a507cd1af493 100644 --- a/homeassistant/components/mjpeg/camera.py +++ b/homeassistant/components/mjpeg/camera.py @@ -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) diff --git a/homeassistant/components/mochad/light.py b/homeassistant/components/mochad/light.py index eff8e406e0e9..1961d7b3a00a 100644 --- a/homeassistant/components/mochad/light.py +++ b/homeassistant/components/mochad/light.py @@ -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) diff --git a/homeassistant/components/mochad/switch.py b/homeassistant/components/mochad/switch.py index 085122cf1d3a..00333b8bc3f8 100644 --- a/homeassistant/components/mochad/switch.py +++ b/homeassistant/components/mochad/switch.py @@ -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) diff --git a/homeassistant/components/modern_forms/fan.py b/homeassistant/components/modern_forms/fan.py index a626f10ecbe7..5220f1a124ad 100644 --- a/homeassistant/components/modern_forms/fan.py +++ b/homeassistant/components/modern_forms/fan.py @@ -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, diff --git a/homeassistant/components/modern_forms/light.py b/homeassistant/components/modern_forms/light.py index 5fa07adf06b0..e2327f3ed072 100644 --- a/homeassistant/components/modern_forms/light.py +++ b/homeassistant/components/modern_forms/light.py @@ -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, diff --git a/homeassistant/components/modern_forms/switch.py b/homeassistant/components/modern_forms/switch.py index 76436a0dd62e..eb131fd2b1ce 100644 --- a/homeassistant/components/modern_forms/switch.py +++ b/homeassistant/components/modern_forms/switch.py @@ -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.""" diff --git a/homeassistant/components/msteams/notify.py b/homeassistant/components/msteams/notify.py index 0c997837f498..4f7b9ba0abcd 100644 --- a/homeassistant/components/msteams/notify.py +++ b/homeassistant/components/msteams/notify.py @@ -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) diff --git a/homeassistant/components/mystrom/light.py b/homeassistant/components/mystrom/light.py index 78c43fc130ac..1ca084d6fed7 100644 --- a/homeassistant/components/mystrom/light.py +++ b/homeassistant/components/mystrom/light.py @@ -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") diff --git a/homeassistant/components/mystrom/switch.py b/homeassistant/components/mystrom/switch.py index 770a5a35b456..bb1132fe43cf 100644 --- a/homeassistant/components/mystrom/switch.py +++ b/homeassistant/components/mystrom/switch.py @@ -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") diff --git a/homeassistant/components/neato/switch.py b/homeassistant/components/neato/switch.py index 411cf17f8e6c..e50d6df872ce 100644 --- a/homeassistant/components/neato/switch.py +++ b/homeassistant/components/neato/switch.py @@ -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 diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index 8c5a5d5ed204..2509fc247669 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -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 diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index bdcd74295b66..552c20152f34 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -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: diff --git a/homeassistant/components/netgear_lte/notify.py b/homeassistant/components/netgear_lte/notify.py index b83701bf0287..8025b29e4db4 100644 --- a/homeassistant/components/netgear_lte/notify.py +++ b/homeassistant/components/netgear_lte/notify.py @@ -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) diff --git a/homeassistant/components/nfandroidtv/notify.py b/homeassistant/components/nfandroidtv/notify.py index 6a0b311a3afb..8a078833db9b 100644 --- a/homeassistant/components/nfandroidtv/notify.py +++ b/homeassistant/components/nfandroidtv/notify.py @@ -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) diff --git a/homeassistant/components/numato/switch.py b/homeassistant/components/numato/switch.py index 997f3956bee1..72756050d38a 100644 --- a/homeassistant/components/numato/switch.py +++ b/homeassistant/components/numato/switch.py @@ -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", diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 2351d95206e6..2678c72f9f28 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -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", diff --git a/homeassistant/components/opendisplay/services.py b/homeassistant/components/opendisplay/services.py index 90ba9396f0db..80b8c1db3b12 100644 --- a/homeassistant/components/opendisplay/services.py +++ b/homeassistant/components/opendisplay/services.py @@ -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 diff --git a/homeassistant/components/openhome/media_player.py b/homeassistant/components/openhome/media_player.py index 4bb055b64c09..3baa45734822 100644 --- a/homeassistant/components/openhome/media_player.py +++ b/homeassistant/components/openhome/media_player.py @@ -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.""" diff --git a/homeassistant/components/pi_hole/switch.py b/homeassistant/components/pi_hole/switch.py index fe369facc0b5..3a5de92ce333 100644 --- a/homeassistant/components/pi_hole/switch.py +++ b/homeassistant/components/pi_hole/switch.py @@ -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) diff --git a/homeassistant/components/pilight/__init__.py b/homeassistant/components/pilight/__init__.py index 50289ff2e964..2cb83de66884 100644 --- a/homeassistant/components/pilight/__init__.py +++ b/homeassistant/components/pilight/__init__.py @@ -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)) diff --git a/homeassistant/components/prosegur/camera.py b/homeassistant/components/prosegur/camera.py index 108792546e32..24f0ddf91635 100644 --- a/homeassistant/components/prosegur/camera.py +++ b/homeassistant/components/prosegur/camera.py @@ -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", diff --git a/homeassistant/components/pushover/notify.py b/homeassistant/components/pushover/notify.py index af209ea66ead..6d1169a4fafc 100644 --- a/homeassistant/components/pushover/notify.py +++ b/homeassistant/components/pushover/notify.py @@ -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. diff --git a/homeassistant/components/qvr_pro/camera.py b/homeassistant/components/qvr_pro/camera.py index 5efdd5629266..b5efff083cfd 100644 --- a/homeassistant/components/qvr_pro/camera.py +++ b/homeassistant/components/qvr_pro/camera.py @@ -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() diff --git a/homeassistant/components/rest/__init__.py b/homeassistant/components/rest/__init__.py index 2f5ae3b7410f..af64d496c6ce 100644 --- a/homeassistant/components/rest/__init__.py +++ b/homeassistant/components/rest/__init__.py @@ -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: diff --git a/homeassistant/components/rest/switch.py b/homeassistant/components/rest/switch.py index 1affeb01d8d0..0f616aedb8b1 100644 --- a/homeassistant/components/rest/switch.py +++ b/homeassistant/components/rest/switch.py @@ -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) diff --git a/homeassistant/components/roomba/vacuum.py b/homeassistant/components/roomba/vacuum.py index ed0c3cc6ee3d..554ab1cbac86 100644 --- a/homeassistant/components/roomba/vacuum.py +++ b/homeassistant/components/roomba/vacuum.py @@ -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'", diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index b86450b0396e..2559a42f109d 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -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 diff --git a/homeassistant/components/schluter/climate.py b/homeassistant/components/schluter/climate.py index 661b6d8fb646..5ce65fa14ab3 100644 --- a/homeassistant/components/schluter/climate.py +++ b/homeassistant/components/schluter/climate.py @@ -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) diff --git a/homeassistant/components/shell_command/__init__.py b/homeassistant/components/shell_command/__init__.py index 3c25b0243474..61863489ce84 100644 --- a/homeassistant/components/shell_command/__init__.py +++ b/homeassistant/components/shell_command/__init__.py @@ -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 diff --git a/homeassistant/components/shopping_list/__init__.py b/homeassistant/components/shopping_list/__init__.py index 93a5626d779a..69f752c2bfed 100644 --- a/homeassistant/components/shopping_list/__init__.py +++ b/homeassistant/components/shopping_list/__init__.py @@ -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: diff --git a/homeassistant/components/simplepush/notify.py b/homeassistant/components/simplepush/notify.py index fcd31e46f8eb..9682d8cd1224 100644 --- a/homeassistant/components/simplepush/notify.py +++ b/homeassistant/components/simplepush/notify.py @@ -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: diff --git a/homeassistant/components/sinch/notify.py b/homeassistant/components/sinch/notify.py index 6954ca6a6e7b..123961cdddc9 100644 --- a/homeassistant/components/sinch/notify.py +++ b/homeassistant/components/sinch/notify.py @@ -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)", diff --git a/homeassistant/components/sky_remote/remote.py b/homeassistant/components/sky_remote/remote.py index 1ecd6c3716eb..ffd4bf848cc6 100644 --- a/homeassistant/components/sky_remote/remote.py +++ b/homeassistant/components/sky_remote/remote.py @@ -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 diff --git a/homeassistant/components/slack/notify.py b/homeassistant/components/slack/notify.py index 444686b50beb..87a10e8cbbe0 100644 --- a/homeassistant/components/slack/notify.py +++ b/homeassistant/components/slack/notify.py @@ -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 = {} diff --git a/homeassistant/components/smlight/light.py b/homeassistant/components/smlight/light.py index 669f6ef03af6..d00678f820a2 100644 --- a/homeassistant/components/smlight/light.py +++ b/homeassistant/components/smlight/light.py @@ -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 diff --git a/homeassistant/components/smlight/update.py b/homeassistant/components/smlight/update.py index fe6d5980b3a2..c5fb90e2cb73 100644 --- a/homeassistant/components/smlight/update.py +++ b/homeassistant/components/smlight/update.py @@ -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", diff --git a/homeassistant/components/synology_dsm/services.py b/homeassistant/components/synology_dsm/services.py index 6ff70ae85bad..ee80bfdcbd04 100644 --- a/homeassistant/components/synology_dsm/services.py +++ b/homeassistant/components/synology_dsm/services.py @@ -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", diff --git a/homeassistant/components/template/__init__.py b/homeassistant/components/template/__init__.py index ba880009e863..606f74c9a309 100644 --- a/homeassistant/components/template/__init__.py +++ b/homeassistant/components/template/__init__.py @@ -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 diff --git a/homeassistant/components/toon/climate.py b/homeassistant/components/toon/climate.py index b5dded48c20d..1ae2a9425f9c 100644 --- a/homeassistant/components/toon/climate.py +++ b/homeassistant/components/toon/climate.py @@ -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.""" diff --git a/homeassistant/components/toon/switch.py b/homeassistant/components/toon/switch.py index c941a2a45f0f..49cfc9a9eb88 100644 --- a/homeassistant/components/toon/switch.py +++ b/homeassistant/components/toon/switch.py @@ -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.""" diff --git a/homeassistant/components/tractive/switch.py b/homeassistant/components/tractive/switch.py index 728dfb94379a..19faf101d7a1 100644 --- a/homeassistant/components/tractive/switch.py +++ b/homeassistant/components/tractive/switch.py @@ -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 diff --git a/homeassistant/components/twilio_call/notify.py b/homeassistant/components/twilio_call/notify.py index 71dfe7267ffe..7f2d45f9002c 100644 --- a/homeassistant/components/twilio_call/notify.py +++ b/homeassistant/components/twilio_call/notify.py @@ -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) diff --git a/homeassistant/components/unifi_access/image.py b/homeassistant/components/unifi_access/image.py index e7e90560718d..90a0ae38872e 100644 --- a/homeassistant/components/unifi_access/image.py +++ b/homeassistant/components/unifi_access/image.py @@ -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", diff --git a/homeassistant/components/vallox/services.py b/homeassistant/components/vallox/services.py index 6bedbe5ab01a..c41498ee4046 100644 --- a/homeassistant/components/vallox/services.py +++ b/homeassistant/components/vallox/services.py @@ -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", diff --git a/homeassistant/components/velbus/services.py b/homeassistant/components/velbus/services.py index 119037886142..8492406beee6 100644 --- a/homeassistant/components/velbus/services.py +++ b/homeassistant/components/velbus/services.py @@ -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: diff --git a/homeassistant/components/vlc_telnet/media_player.py b/homeassistant/components/vlc_telnet/media_player.py index 5e512d41fd05..3fcaa116a3fe 100644 --- a/homeassistant/components/vlc_telnet/media_player.py +++ b/homeassistant/components/vlc_telnet/media_player.py @@ -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.""" diff --git a/homeassistant/components/xeoma/camera.py b/homeassistant/components/xeoma/camera.py index 5a61ffed4620..6fe1b8fe225b 100644 --- a/homeassistant/components/xeoma/camera.py +++ b/homeassistant/components/xeoma/camera.py @@ -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) diff --git a/homeassistant/components/xiaomi/camera.py b/homeassistant/components/xiaomi/camera.py index 72764c6bf521..ea27b526402e 100644 --- a/homeassistant/components/xiaomi/camera.py +++ b/homeassistant/components/xiaomi/camera.py @@ -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 diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index c42a92c0ecfb..a4b3850c10c9 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -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", diff --git a/homeassistant/components/yamaha/media_player.py b/homeassistant/components/yamaha/media_player.py index 0765fad31345..0753b44335cc 100644 --- a/homeassistant/components/yamaha/media_player.py +++ b/homeassistant/components/yamaha/media_player.py @@ -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) diff --git a/homeassistant/components/yamaha_musiccast/media_player.py b/homeassistant/components/yamaha_musiccast/media_player.py index a617ef0b9d0b..5c99b6c60140 100644 --- a/homeassistant/components/yamaha_musiccast/media_player.py +++ b/homeassistant/components/yamaha_musiccast/media_player.py @@ -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( ( diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 9821ec50ac85..56a0cd1dd350 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -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) diff --git a/pylint/plugins/pylint_home_assistant/checkers/actions/__init__.py b/pylint/plugins/pylint_home_assistant/checkers/actions/__init__.py new file mode 100644 index 000000000000..92269ef14c54 --- /dev/null +++ b/pylint/plugins/pylint_home_assistant/checkers/actions/__init__.py @@ -0,0 +1 @@ +"""Action/service related checkers.""" diff --git a/pylint/plugins/pylint_home_assistant/checkers/actions/const.py b/pylint/plugins/pylint_home_assistant/checkers/actions/const.py new file mode 100644 index 000000000000..25b6c84c5cdb --- /dev/null +++ b/pylint/plugins/pylint_home_assistant/checkers/actions/const.py @@ -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", + }, +} diff --git a/pylint/plugins/pylint_home_assistant/checkers/actions/helpers.py b/pylint/plugins/pylint_home_assistant/checkers/actions/helpers.py new file mode 100644 index 000000000000..453ba340f30f --- /dev/null +++ b/pylint/plugins/pylint_home_assistant/checkers/actions/helpers.py @@ -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 diff --git a/pylint/plugins/pylint_home_assistant/checkers/actions/swallowed_exceptions.py b/pylint/plugins/pylint_home_assistant/checkers/actions/swallowed_exceptions.py new file mode 100644 index 000000000000..c2d9fe951795 --- /dev/null +++ b/pylint/plugins/pylint_home_assistant/checkers/actions/swallowed_exceptions.py @@ -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)) diff --git a/tests/pylint/actions/__init__.py b/tests/pylint/actions/__init__.py new file mode 100644 index 000000000000..fc356f5d9fe4 --- /dev/null +++ b/tests/pylint/actions/__init__.py @@ -0,0 +1 @@ +"""Tests for action/service checkers.""" diff --git a/tests/pylint/actions/test_swallowed_exceptions.py b/tests/pylint/actions/test_swallowed_exceptions.py new file mode 100644 index 000000000000..a93b72e9a101 --- /dev/null +++ b/tests/pylint/actions/test_swallowed_exceptions.py @@ -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)