diff --git a/homeassistant/components/airos/__init__.py b/homeassistant/components/airos/__init__.py index 828b28bfe61..fca7b7c2405 100644 --- a/homeassistant/components/airos/__init__.py +++ b/homeassistant/components/airos/__init__.py @@ -81,8 +81,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> boo ) as err: raise ConfigEntryAuthFailed from err except AirOSKeyDataMissingError as err: + # pylint: disable-next=home-assistant-exception-not-translated raise ConfigEntryError("key_data_missing") from err except Exception as err: + # pylint: disable-next=home-assistant-exception-not-translated raise ConfigEntryError("unknown") from err airos_class: type[AirOS8 | AirOS6] = ( diff --git a/homeassistant/components/alexa_devices/coordinator.py b/homeassistant/components/alexa_devices/coordinator.py index a5414722baa..76af2668bf1 100644 --- a/homeassistant/components/alexa_devices/coordinator.py +++ b/homeassistant/components/alexa_devices/coordinator.py @@ -91,6 +91,7 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]): translation_placeholders={"error": repr(err)}, ) from err except CannotAuthenticate as err: + # pylint: disable-next=home-assistant-exception-translation-key-missing raise ConfigEntryAuthFailed( translation_domain=DOMAIN, translation_key="invalid_auth", diff --git a/homeassistant/components/autarco/quality_scale.yaml b/homeassistant/components/autarco/quality_scale.yaml index df1b982be10..23a89e18c84 100644 --- a/homeassistant/components/autarco/quality_scale.yaml +++ b/homeassistant/components/autarco/quality_scale.yaml @@ -82,7 +82,7 @@ rules: comment: | This integration does not have any entities that should disabled by default. entity-translations: done - exception-translations: done + exception-translations: todo icon-translations: done reconfiguration-flow: todo repair-issues: diff --git a/homeassistant/components/autoskope/quality_scale.yaml b/homeassistant/components/autoskope/quality_scale.yaml index 264d9c35e7a..888191d9f63 100644 --- a/homeassistant/components/autoskope/quality_scale.yaml +++ b/homeassistant/components/autoskope/quality_scale.yaml @@ -67,7 +67,7 @@ rules: comment: | Only one entity type (device_tracker) is created, making this not applicable. entity-translations: done - exception-translations: done + exception-translations: todo icon-translations: done reconfiguration-flow: status: todo diff --git a/homeassistant/components/aws_s3/__init__.py b/homeassistant/components/aws_s3/__init__.py index e28f22634f8..9a2339b1740 100644 --- a/homeassistant/components/aws_s3/__init__.py +++ b/homeassistant/components/aws_s3/__init__.py @@ -51,6 +51,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: S3ConfigEntry) -> bool: translation_key="invalid_bucket_name", ) from err except ValueError as err: + # pylint: disable-next=home-assistant-exception-translation-key-missing raise ConfigEntryError( translation_domain=DOMAIN, translation_key="invalid_endpoint_url", diff --git a/homeassistant/components/aws_s3/quality_scale.yaml b/homeassistant/components/aws_s3/quality_scale.yaml index 49c3ea4e35c..5cd5c576bd3 100644 --- a/homeassistant/components/aws_s3/quality_scale.yaml +++ b/homeassistant/components/aws_s3/quality_scale.yaml @@ -72,7 +72,7 @@ rules: entity-device-class: done entity-disabled-by-default: done entity-translations: done - exception-translations: done + exception-translations: todo icon-translations: status: exempt comment: This integration does not use icons. diff --git a/homeassistant/components/azure_storage/backup.py b/homeassistant/components/azure_storage/backup.py index 8816d896df5..c0e5a5c269c 100644 --- a/homeassistant/components/azure_storage/backup.py +++ b/homeassistant/components/azure_storage/backup.py @@ -75,11 +75,13 @@ def handle_backup_errors[_R, **P]( err.message, exc_info=True, ) + # pylint: disable-next=home-assistant-exception-not-translated raise BackupAgentError( f"Error during backup operation in {func.__name__}:" f" Status {err.status_code}, message: {err.message}" ) from err except ServiceRequestError as err: + # pylint: disable-next=home-assistant-exception-not-translated raise BackupAgentError( f"Timeout during backup operation in {func.__name__}" ) from err @@ -90,6 +92,7 @@ def handle_backup_errors[_R, **P]( err, exc_info=True, ) + # pylint: disable-next=home-assistant-exception-not-translated raise BackupAgentError( f"Error during backup operation in {func.__name__}: {err}" ) from err @@ -118,6 +121,7 @@ class AzureStorageBackupAgent(BackupAgent): """Download a backup file.""" blob = await self._find_blob_by_backup_id(backup_id) if blob is None: + # pylint: disable-next=home-assistant-exception-not-translated raise BackupNotFound(f"Backup {backup_id} not found") download_stream = await self._client.download_blob(blob.name) return download_stream.chunks() @@ -155,6 +159,7 @@ class AzureStorageBackupAgent(BackupAgent): """Delete a backup file.""" blob = await self._find_blob_by_backup_id(backup_id) if blob is None: + # pylint: disable-next=home-assistant-exception-not-translated raise BackupNotFound(f"Backup {backup_id} not found") await self._client.delete_blob(blob.name) @@ -181,6 +186,7 @@ class AzureStorageBackupAgent(BackupAgent): """Return a backup.""" blob = await self._find_blob_by_backup_id(backup_id) if blob is None: + # pylint: disable-next=home-assistant-exception-not-translated raise BackupNotFound(f"Backup {backup_id} not found") return AgentBackup.from_dict(json.loads(blob.metadata["backup_metadata"])) diff --git a/homeassistant/components/backblaze_b2/__init__.py b/homeassistant/components/backblaze_b2/__init__.py index 120172ae27e..219ba4cc9ef 100644 --- a/homeassistant/components/backblaze_b2/__init__.py +++ b/homeassistant/components/backblaze_b2/__init__.py @@ -89,6 +89,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: BackblazeConfigEntry) -> translation_key="cannot_connect", ) from err except exception.MissingAccountData as err: + # pylint: disable-next=home-assistant-exception-translation-key-missing raise ConfigEntryAuthFailed( translation_domain=DOMAIN, translation_key="invalid_auth", diff --git a/homeassistant/components/backblaze_b2/quality_scale.yaml b/homeassistant/components/backblaze_b2/quality_scale.yaml index f3c388a31de..d9c0e5e2647 100644 --- a/homeassistant/components/backblaze_b2/quality_scale.yaml +++ b/homeassistant/components/backblaze_b2/quality_scale.yaml @@ -96,7 +96,7 @@ rules: entity-translations: status: exempt comment: This integration does not have entities. - exception-translations: done + exception-translations: todo icon-translations: status: exempt comment: This integration does not use icons. diff --git a/homeassistant/components/blink/camera.py b/homeassistant/components/blink/camera.py index a5b1b05c410..57865984a45 100644 --- a/homeassistant/components/blink/camera.py +++ b/homeassistant/components/blink/camera.py @@ -169,6 +169,7 @@ class BlinkCamera(CoordinatorEntity[BlinkUpdateCoordinator], Camera): try: await self._camera.save_recent_clips(output_dir=file_path) except OSError as err: + # pylint: disable-next=home-assistant-exception-message-with-translation raise ServiceValidationError( str(err), translation_domain=DOMAIN, @@ -190,6 +191,7 @@ class BlinkCamera(CoordinatorEntity[BlinkUpdateCoordinator], Camera): try: await self._camera.video_to_file(filename) except OSError as err: + # pylint: disable-next=home-assistant-exception-message-with-translation raise ServiceValidationError( str(err), translation_domain=DOMAIN, diff --git a/homeassistant/components/bsblan/climate.py b/homeassistant/components/bsblan/climate.py index 32cd3b4c9b2..ac2387b7b7d 100644 --- a/homeassistant/components/bsblan/climate.py +++ b/homeassistant/components/bsblan/climate.py @@ -183,6 +183,7 @@ class BSBLANClimate(BSBLanCircuitEntity, ClimateEntity): try: await self.coordinator.client.thermostat(**data, circuit=self._circuit) except BSBLANError as err: + # pylint: disable-next=home-assistant-exception-message-with-translation raise HomeAssistantError( "An error occurred while updating the BSBLAN device", translation_domain=DOMAIN, diff --git a/homeassistant/components/cloudflare_r2/quality_scale.yaml b/homeassistant/components/cloudflare_r2/quality_scale.yaml index 9b9ed3e6619..36a8c0c8676 100644 --- a/homeassistant/components/cloudflare_r2/quality_scale.yaml +++ b/homeassistant/components/cloudflare_r2/quality_scale.yaml @@ -94,7 +94,7 @@ rules: entity-translations: status: exempt comment: This integration does not have entities. - exception-translations: done + exception-translations: todo icon-translations: status: exempt comment: This integration does not use icons. diff --git a/homeassistant/components/comelit/config_flow.py b/homeassistant/components/comelit/config_flow.py index d91897b05c1..c626e78e5cc 100644 --- a/homeassistant/components/comelit/config_flow.py +++ b/homeassistant/components/comelit/config_flow.py @@ -65,6 +65,7 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, translation_placeholders={"error": repr(err)}, ) from err except aiocomelit_exceptions.CannotAuthenticate as err: + # pylint: disable-next=home-assistant-exception-placeholder-mismatch raise InvalidAuth( translation_domain=DOMAIN, translation_key="cannot_authenticate", diff --git a/homeassistant/components/ecovacs/vacuum.py b/homeassistant/components/ecovacs/vacuum.py index 5c8cfa1b9c9..3ca7c5a50e4 100644 --- a/homeassistant/components/ecovacs/vacuum.py +++ b/homeassistant/components/ecovacs/vacuum.py @@ -353,6 +353,7 @@ class EcovacsVacuum( if self._capability.clean.action.area is None: info = self._device.device_info name = info.get("nick", info["name"]) + # pylint: disable-next=home-assistant-exception-translation-key-missing raise ServiceValidationError( translation_domain=DOMAIN, translation_key="vacuum_send_command_area_not_supported", diff --git a/homeassistant/components/energyid/quality_scale.yaml b/homeassistant/components/energyid/quality_scale.yaml index ff4ed64e2c9..c84f410d2ac 100644 --- a/homeassistant/components/energyid/quality_scale.yaml +++ b/homeassistant/components/energyid/quality_scale.yaml @@ -88,7 +88,7 @@ rules: entity-translations: status: exempt comment: This integration does not create its own entities. - exception-translations: done + exception-translations: todo icon-translations: status: exempt comment: This integration does not create its own entities. diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index c92eb57faff..13052c526da 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -364,6 +364,7 @@ class ESPHomeManager: response_dict = {"response": response} except TemplateError as ex: + # pylint: disable-next=home-assistant-exception-not-translated raise HomeAssistantError( f"Error rendering response template: {ex}" ) from ex diff --git a/homeassistant/components/fressnapf_tracker/quality_scale.yaml b/homeassistant/components/fressnapf_tracker/quality_scale.yaml index 39614e94b66..f1500b496df 100644 --- a/homeassistant/components/fressnapf_tracker/quality_scale.yaml +++ b/homeassistant/components/fressnapf_tracker/quality_scale.yaml @@ -63,7 +63,7 @@ rules: comment: | This integration does not have many entities. All of them are fundamental. entity-translations: done - exception-translations: done + exception-translations: todo icon-translations: done reconfiguration-flow: done repair-issues: todo diff --git a/homeassistant/components/fritz/__init__.py b/homeassistant/components/fritz/__init__.py index 9365b2fea32..4a691e70231 100644 --- a/homeassistant/components/fritz/__init__.py +++ b/homeassistant/components/fritz/__init__.py @@ -67,6 +67,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: FritzConfigEntry) -> boo "X_AVM-DE_UPnP1" in avm_wrapper.connection.services and not (await avm_wrapper.async_get_upnp_configuration())["NewEnable"] ): + # pylint: disable-next=home-assistant-exception-not-translated raise ConfigEntryAuthFailed("Missing UPnP configuration") await avm_wrapper.async_config_entry_first_refresh() diff --git a/homeassistant/components/google_drive/backup.py b/homeassistant/components/google_drive/backup.py index bd2e81ee739..9d0ea11ddbc 100644 --- a/homeassistant/components/google_drive/backup.py +++ b/homeassistant/components/google_drive/backup.py @@ -100,6 +100,7 @@ class GoogleDriveBackupAgent(BackupAgent): try: await self._client.async_upload_backup(wrapped_open_stream, backup) except (GoogleDriveApiError, HomeAssistantError, TimeoutError) as err: + # pylint: disable-next=home-assistant-exception-not-translated raise BackupAgentError(f"Failed to upload backup: {err}") from err async def async_list_backups(self, **kwargs: Any) -> list[AgentBackup]: @@ -107,6 +108,7 @@ class GoogleDriveBackupAgent(BackupAgent): try: return await self._client.async_list_backups() except (GoogleDriveApiError, HomeAssistantError, TimeoutError) as err: + # pylint: disable-next=home-assistant-exception-not-translated raise BackupAgentError(f"Failed to list backups: {err}") from err async def async_get_backup( @@ -119,6 +121,7 @@ class GoogleDriveBackupAgent(BackupAgent): for backup in backups: if backup.backup_id == backup_id: return backup + # pylint: disable-next=home-assistant-exception-not-translated raise BackupNotFound(f"Backup {backup_id} not found") async def async_download_backup( @@ -139,7 +142,9 @@ class GoogleDriveBackupAgent(BackupAgent): stream = await self._client.async_download(file_id) return ChunkAsyncStreamIterator(stream) except (GoogleDriveApiError, HomeAssistantError, TimeoutError) as err: + # pylint: disable-next=home-assistant-exception-not-translated raise BackupAgentError(f"Failed to download backup: {err}") from err + # pylint: disable-next=home-assistant-exception-not-translated raise BackupNotFound(f"Backup {backup_id} not found") async def async_delete_backup( @@ -160,5 +165,7 @@ class GoogleDriveBackupAgent(BackupAgent): _LOGGER.debug("Deleted backup_id: %s", backup_id) return except (GoogleDriveApiError, HomeAssistantError, TimeoutError) as err: + # pylint: disable-next=home-assistant-exception-not-translated raise BackupAgentError(f"Failed to delete backup: {err}") from err + # pylint: disable-next=home-assistant-exception-not-translated raise BackupNotFound(f"Backup {backup_id} not found") diff --git a/homeassistant/components/google_photos/services.py b/homeassistant/components/google_photos/services.py index 606aba73f88..e43f6b7ac6f 100644 --- a/homeassistant/components/google_photos/services.py +++ b/homeassistant/components/google_photos/services.py @@ -84,6 +84,7 @@ async def _async_handle_upload(call: ServiceCall) -> ServiceResponse: scopes = config_entry.data["token"]["scope"].split(" ") if UPLOAD_SCOPE not in scopes: + # pylint: disable-next=home-assistant-exception-placeholder-mismatch raise HomeAssistantError( translation_domain=DOMAIN, translation_key="missing_upload_permission", diff --git a/homeassistant/components/growatt_server/quality_scale.yaml b/homeassistant/components/growatt_server/quality_scale.yaml index 15502bdc5b0..c6c503981c3 100644 --- a/homeassistant/components/growatt_server/quality_scale.yaml +++ b/homeassistant/components/growatt_server/quality_scale.yaml @@ -56,7 +56,7 @@ rules: entity-device-class: done entity-disabled-by-default: done entity-translations: done - exception-translations: done + exception-translations: todo icon-translations: done reconfiguration-flow: done repair-issues: diff --git a/homeassistant/components/habitica/notify.py b/homeassistant/components/habitica/notify.py index 64c98a86fae..284642bd33f 100644 --- a/homeassistant/components/habitica/notify.py +++ b/homeassistant/components/habitica/notify.py @@ -109,6 +109,7 @@ class HabiticaBaseNotifyEntity(HabiticaBase, NotifyEntity): try: await self._send_message(message) except NotAuthorizedError as e: + # pylint: disable-next=home-assistant-exception-placeholder-mismatch raise HomeAssistantError( translation_domain=DOMAIN, translation_key="send_message_forbidden", @@ -118,6 +119,7 @@ class HabiticaBaseNotifyEntity(HabiticaBase, NotifyEntity): }, ) from e except NotFoundError as e: + # pylint: disable-next=home-assistant-exception-placeholder-mismatch raise HomeAssistantError( translation_domain=DOMAIN, translation_key="send_message_not_found", diff --git a/homeassistant/components/home_connect/climate.py b/homeassistant/components/home_connect/climate.py index 3904a697eaa..38b92412695 100644 --- a/homeassistant/components/home_connect/climate.py +++ b/homeassistant/components/home_connect/climate.py @@ -266,6 +266,7 @@ class HomeConnectAirConditioningEntity(HomeConnectEntity, ClimateEntity): value=BSH_POWER_ON, ) except HomeConnectError as err: + # pylint: disable-next=home-assistant-exception-placeholder-mismatch raise HomeAssistantError( translation_domain=DOMAIN, translation_key="power_on", diff --git a/homeassistant/components/homee/quality_scale.yaml b/homeassistant/components/homee/quality_scale.yaml index bf86abb7938..755278e61d4 100644 --- a/homeassistant/components/homee/quality_scale.yaml +++ b/homeassistant/components/homee/quality_scale.yaml @@ -59,7 +59,7 @@ rules: entity-device-class: done entity-disabled-by-default: done entity-translations: done - exception-translations: done + exception-translations: todo icon-translations: done reconfiguration-flow: done repair-issues: todo diff --git a/homeassistant/components/homevolt/entity.py b/homeassistant/components/homevolt/entity.py index fcea7c6da3a..aa029b047b4 100644 --- a/homeassistant/components/homevolt/entity.py +++ b/homeassistant/components/homevolt/entity.py @@ -50,12 +50,14 @@ def homevolt_exception_handler[_HomevoltEntityT: HomevoltEntity, **_P]( translation_key="auth_failed", ) from error except HomevoltConnectionError as error: + # pylint: disable-next=home-assistant-exception-placeholder-mismatch raise HomeAssistantError( translation_domain=DOMAIN, translation_key="communication_error", translation_placeholders={"error": str(error)}, ) from error except HomevoltError as error: + # pylint: disable-next=home-assistant-exception-placeholder-mismatch raise HomeAssistantError( translation_domain=DOMAIN, translation_key="unknown_error", diff --git a/homeassistant/components/homewizard/coordinator.py b/homeassistant/components/homewizard/coordinator.py index c5366ba13b9..1e14ada6e5f 100644 --- a/homeassistant/components/homewizard/coordinator.py +++ b/homeassistant/components/homewizard/coordinator.py @@ -44,6 +44,7 @@ class HWEnergyDeviceUpdateCoordinator(DataUpdateCoordinator[DeviceResponseEntry] data = await self.api.combined() except RequestError as ex: + # pylint: disable-next=home-assistant-exception-message-with-translation raise UpdateFailed( ex, translation_domain=DOMAIN, translation_key="communication_error" ) from ex @@ -60,6 +61,7 @@ class HWEnergyDeviceUpdateCoordinator(DataUpdateCoordinator[DeviceResponseEntry] self.config_entry.entry_id ) + # pylint: disable-next=home-assistant-exception-message-with-translation raise UpdateFailed( ex, translation_domain=DOMAIN, translation_key="api_disabled" ) from ex diff --git a/homeassistant/components/husqvarna_automower/quality_scale.yaml b/homeassistant/components/husqvarna_automower/quality_scale.yaml index d0435c51eee..f98dd4770fd 100644 --- a/homeassistant/components/husqvarna_automower/quality_scale.yaml +++ b/homeassistant/components/husqvarna_automower/quality_scale.yaml @@ -62,7 +62,7 @@ rules: entity-device-class: done entity-disabled-by-default: done entity-translations: done - exception-translations: done + exception-translations: todo icon-translations: done reconfiguration-flow: status: exempt diff --git a/homeassistant/components/idrive_e2/quality_scale.yaml b/homeassistant/components/idrive_e2/quality_scale.yaml index 11093f4430f..9bcf1f82f1e 100644 --- a/homeassistant/components/idrive_e2/quality_scale.yaml +++ b/homeassistant/components/idrive_e2/quality_scale.yaml @@ -94,7 +94,7 @@ rules: entity-translations: status: exempt comment: This integration does not have entities. - exception-translations: done + exception-translations: todo icon-translations: status: exempt comment: This integration does not use icons. diff --git a/homeassistant/components/imap/quality_scale.yaml b/homeassistant/components/imap/quality_scale.yaml index 1c75b527882..ff7dd8104a3 100644 --- a/homeassistant/components/imap/quality_scale.yaml +++ b/homeassistant/components/imap/quality_scale.yaml @@ -62,7 +62,7 @@ rules: comment: > The device class is a service. When removed, entities are removed as well. diagnostics: done - exception-translations: done + exception-translations: todo icon-translations: done reconfiguration-flow: status: todo diff --git a/homeassistant/components/incomfort/__init__.py b/homeassistant/components/incomfort/__init__.py index b1daaf7dfeb..26137b2f036 100644 --- a/homeassistant/components/incomfort/__init__.py +++ b/homeassistant/components/incomfort/__init__.py @@ -73,6 +73,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: InComfortConfigEntry) -> except InvalidHeaterList as exc: raise NoHeaters from exc except InvalidGateway as exc: + # pylint: disable-next=home-assistant-exception-not-translated raise ConfigEntryAuthFailed("Incorrect credentials") from exc except ClientResponseError as exc: if exc.status == 404: diff --git a/homeassistant/components/incomfort/coordinator.py b/homeassistant/components/incomfort/coordinator.py index 5c72b9daa06..ad3a6f4fa86 100644 --- a/homeassistant/components/incomfort/coordinator.py +++ b/homeassistant/components/incomfort/coordinator.py @@ -78,11 +78,15 @@ class InComfortDataCoordinator(DataUpdateCoordinator[InComfortData]): for heater in self.incomfort_data.heaters: await heater.update() except TimeoutError as exc: + # pylint: disable-next=home-assistant-exception-not-translated raise UpdateFailed("Timeout error") from exc except ClientResponseError as exc: if exc.status == 401: + # pylint: disable-next=home-assistant-exception-not-translated raise ConfigEntryError("Incorrect credentials") from exc + # pylint: disable-next=home-assistant-exception-not-translated raise UpdateFailed(exc.message) from exc except InvalidHeaterList as exc: + # pylint: disable-next=home-assistant-exception-not-translated raise UpdateFailed(exc.message) from exc return self.incomfort_data diff --git a/homeassistant/components/indevolt/quality_scale.yaml b/homeassistant/components/indevolt/quality_scale.yaml index f93e6679f3c..01fe0176283 100644 --- a/homeassistant/components/indevolt/quality_scale.yaml +++ b/homeassistant/components/indevolt/quality_scale.yaml @@ -60,7 +60,7 @@ rules: entity-device-class: done entity-disabled-by-default: done entity-translations: done - exception-translations: done + exception-translations: todo icon-translations: done reconfiguration-flow: done repair-issues: diff --git a/homeassistant/components/intelliclima/quality_scale.yaml b/homeassistant/components/intelliclima/quality_scale.yaml index e66578de063..a9ce60b33df 100644 --- a/homeassistant/components/intelliclima/quality_scale.yaml +++ b/homeassistant/components/intelliclima/quality_scale.yaml @@ -60,7 +60,7 @@ rules: entity-device-class: done entity-disabled-by-default: done entity-translations: done - exception-translations: done + exception-translations: todo icon-translations: done reconfiguration-flow: todo repair-issues: done diff --git a/homeassistant/components/israel_rail/__init__.py b/homeassistant/components/israel_rail/__init__.py index ed800f559d4..ebe69625970 100644 --- a/homeassistant/components/israel_rail/__init__.py +++ b/homeassistant/components/israel_rail/__init__.py @@ -29,6 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: IsraelRailConfigEntry) - try: await hass.async_add_executor_job(train_schedule.query, start, destination) except Exception as e: + # pylint: disable-next=home-assistant-exception-translation-key-missing raise ConfigEntryNotReady( translation_domain=DOMAIN, translation_key="request_timeout", diff --git a/homeassistant/components/knx/device_trigger.py b/homeassistant/components/knx/device_trigger.py index 74e72a684c4..e3d5141c1ce 100644 --- a/homeassistant/components/knx/device_trigger.py +++ b/homeassistant/components/knx/device_trigger.py @@ -115,6 +115,7 @@ async def async_attach_trigger( try: trigger_config = TRIGGER_TRIGGER_SCHEMA(trigger_config) except vol.Invalid as err: + # pylint: disable-next=home-assistant-exception-not-translated raise InvalidDeviceAutomationConfig(f"{err}") from err return await trigger.async_attach_trigger( diff --git a/homeassistant/components/letpot/quality_scale.yaml b/homeassistant/components/letpot/quality_scale.yaml index 3931e7eabed..4d572906bdb 100644 --- a/homeassistant/components/letpot/quality_scale.yaml +++ b/homeassistant/components/letpot/quality_scale.yaml @@ -66,7 +66,7 @@ rules: entity-device-class: done entity-disabled-by-default: done entity-translations: done - exception-translations: done + exception-translations: todo icon-translations: done reconfiguration-flow: todo repair-issues: todo diff --git a/homeassistant/components/lg_thinq/entity.py b/homeassistant/components/lg_thinq/entity.py index f10e8821f6d..63dec0f5e96 100644 --- a/homeassistant/components/lg_thinq/entity.py +++ b/homeassistant/components/lg_thinq/entity.py @@ -105,6 +105,7 @@ class ThinQEntity(CoordinatorEntity[DeviceDataUpdateCoordinator]): except ThinQAPIException as exc: if on_fail_method: on_fail_method() + # pylint: disable-next=home-assistant-exception-message-with-translation raise ServiceValidationError( exc.message, translation_domain=DOMAIN, translation_key=exc.code ) from exc diff --git a/homeassistant/components/liebherr/__init__.py b/homeassistant/components/liebherr/__init__.py index 753f2560557..1fa5231a8db 100644 --- a/homeassistant/components/liebherr/__init__.py +++ b/homeassistant/components/liebherr/__init__.py @@ -44,8 +44,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: LiebherrConfigEntry) -> try: devices = await client.get_devices() except LiebherrAuthenticationError as err: + # pylint: disable-next=home-assistant-exception-not-translated raise ConfigEntryAuthFailed("Invalid API key") from err except LiebherrConnectionError as err: + # pylint: disable-next=home-assistant-exception-not-translated raise ConfigEntryNotReady(f"Failed to connect to Liebherr API: {err}") from err # Create a coordinator for each device (may be empty if no devices) diff --git a/homeassistant/components/liebherr/coordinator.py b/homeassistant/components/liebherr/coordinator.py index c66829a167c..6dd746a21e6 100644 --- a/homeassistant/components/liebherr/coordinator.py +++ b/homeassistant/components/liebherr/coordinator.py @@ -58,8 +58,10 @@ class LiebherrCoordinator(DataUpdateCoordinator[DeviceState]): try: await self.client.get_device(self.device_id) except LiebherrAuthenticationError as err: + # pylint: disable-next=home-assistant-exception-not-translated raise ConfigEntryAuthFailed("Invalid API key") from err except LiebherrConnectionError as err: + # pylint: disable-next=home-assistant-exception-not-translated raise ConfigEntryNotReady( f"Failed to connect to device {self.device_id}: {err}" ) from err @@ -69,12 +71,15 @@ class LiebherrCoordinator(DataUpdateCoordinator[DeviceState]): try: return await self.client.get_device_state(self.device_id) except LiebherrAuthenticationError as err: + # pylint: disable-next=home-assistant-exception-not-translated raise ConfigEntryAuthFailed("API key is no longer valid") from err except LiebherrTimeoutError as err: + # pylint: disable-next=home-assistant-exception-not-translated raise UpdateFailed( f"Timeout communicating with device {self.device_id}" ) from err except LiebherrConnectionError as err: + # pylint: disable-next=home-assistant-exception-not-translated raise UpdateFailed( f"Error communicating with device {self.device_id}" ) from err diff --git a/homeassistant/components/local_file/__init__.py b/homeassistant/components/local_file/__init__.py index 183c8b7ee82..1fc97349b28 100644 --- a/homeassistant/components/local_file/__init__.py +++ b/homeassistant/components/local_file/__init__.py @@ -15,6 +15,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Local file from a config entry.""" file_path: str = entry.options[CONF_FILE_PATH] if not await hass.async_add_executor_job(check_file_path_access, file_path): + # pylint: disable-next=home-assistant-exception-translation-key-missing raise ConfigEntryError( translation_domain=DOMAIN, translation_key="not_readable_path", diff --git a/homeassistant/components/lunatone/__init__.py b/homeassistant/components/lunatone/__init__.py index 5a2f2867475..8f7e8810f4b 100644 --- a/homeassistant/components/lunatone/__init__.py +++ b/homeassistant/components/lunatone/__init__.py @@ -77,6 +77,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: LunatoneConfigEntry) -> await coordinator_info.async_config_entry_first_refresh() if info_api.data is None or info_api.serial_number is None: + # pylint: disable-next=home-assistant-exception-translation-key-missing raise ConfigEntryError( translation_domain=DOMAIN, translation_key="missing_device_info" ) diff --git a/homeassistant/components/manual/alarm_control_panel.py b/homeassistant/components/manual/alarm_control_panel.py index 7bc8264dd38..230df3f3dce 100644 --- a/homeassistant/components/manual/alarm_control_panel.py +++ b/homeassistant/components/manual/alarm_control_panel.py @@ -422,6 +422,7 @@ class ManualAlarm(AlarmControlPanelEntity, RestoreEntity): }, ) + # pylint: disable-next=home-assistant-exception-message-with-translation raise ServiceValidationError( "Invalid alarm code provided", translation_domain=DOMAIN, diff --git a/homeassistant/components/miele/fan.py b/homeassistant/components/miele/fan.py index 4e0a7879cd7..890d27d13bb 100644 --- a/homeassistant/components/miele/fan.py +++ b/homeassistant/components/miele/fan.py @@ -165,6 +165,7 @@ class MieleFan(MieleEntity, FanEntity): try: await self.api.send_action(self._device_id, {POWER_ON: True}) except ClientResponseError as ex: + # pylint: disable-next=home-assistant-exception-placeholder-mismatch raise HomeAssistantError( translation_domain=DOMAIN, translation_key="set_state_error", @@ -182,6 +183,7 @@ class MieleFan(MieleEntity, FanEntity): try: await self.api.send_action(self._device_id, {POWER_OFF: True}) except ClientResponseError as ex: + # pylint: disable-next=home-assistant-exception-placeholder-mismatch raise HomeAssistantError( translation_domain=DOMAIN, translation_key="set_state_error", diff --git a/homeassistant/components/motionmount/quality_scale.yaml b/homeassistant/components/motionmount/quality_scale.yaml index 8b210931eaf..33ea8de5ba7 100644 --- a/homeassistant/components/motionmount/quality_scale.yaml +++ b/homeassistant/components/motionmount/quality_scale.yaml @@ -60,7 +60,7 @@ rules: entity-device-class: done entity-disabled-by-default: done entity-translations: done - exception-translations: done + exception-translations: todo icon-translations: done reconfiguration-flow: todo repair-issues: diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index db439582088..a9d0ad2c36b 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -291,6 +291,7 @@ async def async_check_config_schema( message = conf_util.format_schema_error( hass, exc, domain, config, integration.documentation ) + # pylint: disable-next=home-assistant-exception-message-with-translation raise ServiceValidationError( message, translation_domain=DOMAIN, diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index e3cf4606c17..a828de380c8 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -156,6 +156,7 @@ async def async_publish( ) -> None: """Publish message to a MQTT topic.""" if not mqtt_config_entry_enabled(hass): + # pylint: disable-next=home-assistant-exception-message-with-translation raise HomeAssistantError( f"Cannot publish to topic '{topic}', MQTT is not enabled", translation_key="mqtt_not_setup_cannot_publish", @@ -281,6 +282,7 @@ def async_subscribe_internal( try: mqtt_data = hass.data[DATA_MQTT] except KeyError as exc: + # pylint: disable-next=home-assistant-exception-message-with-translation raise HomeAssistantError( f"Cannot subscribe to topic '{topic}', make sure MQTT is set up correctly", translation_key="mqtt_not_setup_cannot_subscribe", @@ -289,6 +291,7 @@ def async_subscribe_internal( ) from exc client = mqtt_data.client if not mqtt_config_entry_enabled(hass): + # pylint: disable-next=home-assistant-exception-message-with-translation raise HomeAssistantError( f"Cannot subscribe to topic '{topic}', MQTT is not enabled", translation_key="mqtt_not_setup_cannot_subscribe", diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index 791e0cb0887..186b5e816c0 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -73,6 +73,7 @@ class SubscriptionID: subscription_id = self._next_id if subscription_id > MAX_28BIT: + # pylint: disable-next=home-assistant-exception-message-with-translation raise HomeAssistantError( "MQTT Subscription ID limit reached. " "Cannot generate more IDs to subscribe", diff --git a/homeassistant/components/nasweb/__init__.py b/homeassistant/components/nasweb/__init__.py index b2351f1aeb6..4b85684bd50 100644 --- a/homeassistant/components/nasweb/__init__.py +++ b/homeassistant/components/nasweb/__init__.py @@ -50,6 +50,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: NASwebConfigEntry) -> bo ) if not await webio_api.refresh_device_info(): _LOGGER.error("[%s] Refresh device info failed", entry.data[CONF_HOST]) + # pylint: disable-next=home-assistant-exception-translation-key-domain-mismatch raise ConfigEntryError( translation_key="config_entry_error_internal_error", translation_placeholders={"support_email": SUPPORT_EMAIL}, @@ -57,6 +58,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: NASwebConfigEntry) -> bo webio_serial = webio_api.get_serial_number() if webio_serial is None: _LOGGER.error("[%s] Serial number not available", entry.data[CONF_HOST]) + # pylint: disable-next=home-assistant-exception-translation-key-domain-mismatch raise ConfigEntryError( translation_key="config_entry_error_internal_error", translation_placeholders={"support_email": SUPPORT_EMAIL}, @@ -65,6 +67,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: NASwebConfigEntry) -> bo _LOGGER.error( "[%s] Serial number doesn't match config entry", entry.data[CONF_HOST] ) + # pylint: disable-next=home-assistant-exception-translation-key-domain-mismatch raise ConfigEntryError(translation_key="config_entry_error_serial_mismatch") coordinator = NASwebCoordinator( @@ -76,12 +79,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: NASwebConfigEntry) -> bo webhook_url = nasweb_data.get_webhook_url(hass) if not await webio_api.status_subscription(webhook_url, True): _LOGGER.error("Failed to subscribe for status updates from webio") + # pylint: disable-next=home-assistant-exception-translation-key-domain-mismatch raise ConfigEntryError( translation_key="config_entry_error_internal_error", translation_placeholders={"support_email": SUPPORT_EMAIL}, ) if not await nasweb_data.notify_coordinator.check_connection(webio_serial): _LOGGER.error("Did not receive status from device") + # pylint: disable-next=home-assistant-exception-translation-key-domain-mismatch raise ConfigEntryError( translation_key="config_entry_error_no_status_update", translation_placeholders={"support_email": SUPPORT_EMAIL}, @@ -91,10 +96,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: NASwebConfigEntry) -> bo f"[{entry.data[CONF_HOST]}] Check connection reached timeout" ) from error except AuthError as error: + # pylint: disable-next=home-assistant-exception-translation-key-domain-mismatch raise ConfigEntryError( translation_key="config_entry_error_invalid_authentication" ) from error except NoURLAvailableError as error: + # pylint: disable-next=home-assistant-exception-translation-key-domain-mismatch raise ConfigEntryError( translation_key="config_entry_error_missing_internal_url" ) from error diff --git a/homeassistant/components/ness_alarm/quality_scale.yaml b/homeassistant/components/ness_alarm/quality_scale.yaml index f2505459407..a27fa1e24af 100644 --- a/homeassistant/components/ness_alarm/quality_scale.yaml +++ b/homeassistant/components/ness_alarm/quality_scale.yaml @@ -66,7 +66,7 @@ rules: entity-translations: status: exempt comment: Entities use device name as entity name. - exception-translations: done + exception-translations: todo icon-translations: status: exempt comment: No entity icons are used. diff --git a/homeassistant/components/nfandroidtv/notify.py b/homeassistant/components/nfandroidtv/notify.py index 0880f11a1a3..931883fa4d7 100644 --- a/homeassistant/components/nfandroidtv/notify.py +++ b/homeassistant/components/nfandroidtv/notify.py @@ -160,6 +160,7 @@ class NFAndroidTVNotificationService(BaseNotificationService): auth=imagedata.get(ATTR_IMAGE_AUTH), ) else: + # pylint: disable-next=home-assistant-exception-message-with-translation raise ServiceValidationError( "Invalid image provided", translation_domain=DOMAIN, @@ -182,6 +183,7 @@ class NFAndroidTVNotificationService(BaseNotificationService): auth=icondata.get(ATTR_ICON_AUTH), ) else: + # pylint: disable-next=home-assistant-exception-message-with-translation raise ServiceValidationError( "Invalid Icon provided", translation_domain=DOMAIN, diff --git a/homeassistant/components/nintendo_parental_controls/coordinator.py b/homeassistant/components/nintendo_parental_controls/coordinator.py index de118f21e2c..ba84c106774 100644 --- a/homeassistant/components/nintendo_parental_controls/coordinator.py +++ b/homeassistant/components/nintendo_parental_controls/coordinator.py @@ -52,6 +52,7 @@ class NintendoUpdateCoordinator(DataUpdateCoordinator[None]): try: return await self.api.update() except InvalidOAuthConfigurationException as err: + # pylint: disable-next=home-assistant-exception-translation-key-missing raise ConfigEntryError( translation_domain=DOMAIN, translation_key="invalid_auth", diff --git a/homeassistant/components/nintendo_parental_controls/quality_scale.yaml b/homeassistant/components/nintendo_parental_controls/quality_scale.yaml index 0cc427fd205..8837d6f779f 100644 --- a/homeassistant/components/nintendo_parental_controls/quality_scale.yaml +++ b/homeassistant/components/nintendo_parental_controls/quality_scale.yaml @@ -57,7 +57,7 @@ rules: entity-device-class: done entity-disabled-by-default: done entity-translations: done - exception-translations: done + exception-translations: todo icon-translations: status: exempt comment: | diff --git a/homeassistant/components/nobo_hub/quality_scale.yaml b/homeassistant/components/nobo_hub/quality_scale.yaml index 9869b9c445e..6a64bb7e0a6 100644 --- a/homeassistant/components/nobo_hub/quality_scale.yaml +++ b/homeassistant/components/nobo_hub/quality_scale.yaml @@ -65,7 +65,7 @@ rules: PR #170135. entity-disabled-by-default: todo entity-translations: todo - exception-translations: done + exception-translations: todo icon-translations: todo reconfiguration-flow: todo repair-issues: diff --git a/homeassistant/components/nrgkick/quality_scale.yaml b/homeassistant/components/nrgkick/quality_scale.yaml index 7bdc82b665b..f2a89caff61 100644 --- a/homeassistant/components/nrgkick/quality_scale.yaml +++ b/homeassistant/components/nrgkick/quality_scale.yaml @@ -66,7 +66,7 @@ rules: entity-device-class: done entity-disabled-by-default: done entity-translations: done - exception-translations: done + exception-translations: todo icon-translations: done reconfiguration-flow: done repair-issues: todo diff --git a/homeassistant/components/onedrive/backup.py b/homeassistant/components/onedrive/backup.py index 10178022e10..c0e82933d34 100644 --- a/homeassistant/components/onedrive/backup.py +++ b/homeassistant/components/onedrive/backup.py @@ -81,6 +81,7 @@ def handle_backup_errors[_R, **P]( return await func(self, *args, **kwargs) except AuthenticationError as err: self._entry.async_start_reauth(self._hass) + # pylint: disable-next=home-assistant-exception-not-translated raise BackupAgentError("Authentication error") from err except OneDriveException as err: _LOGGER.error( @@ -89,12 +90,14 @@ def handle_backup_errors[_R, **P]( err, ) _LOGGER.debug("Full error: %s", err, exc_info=True) + # pylint: disable-next=home-assistant-exception-not-translated raise BackupAgentError("Backup operation failed") from err except TimeoutError as err: _LOGGER.error( "Error during backup in %s: Timeout", func.__name__, ) + # pylint: disable-next=home-assistant-exception-not-translated raise BackupAgentError("Backup operation timed out") from err return wrapper @@ -183,6 +186,7 @@ class OneDriveBackupAgent(BackupAgent): ), ) except HashMismatchError as err: + # pylint: disable-next=home-assistant-exception-not-translated raise BackupAgentError( "Hash validation failed, backup file might be corrupt" ) from err @@ -292,4 +296,5 @@ class OneDriveBackupAgent(BackupAgent): if backup := metadata_files.get(backup_id): return backup + # pylint: disable-next=home-assistant-exception-not-translated raise BackupNotFound(f"Backup {backup_id} not found") diff --git a/homeassistant/components/onedrive_for_business/backup.py b/homeassistant/components/onedrive_for_business/backup.py index 19cf1f63033..d378d00dacc 100644 --- a/homeassistant/components/onedrive_for_business/backup.py +++ b/homeassistant/components/onedrive_for_business/backup.py @@ -81,6 +81,7 @@ def handle_backup_errors[_R, **P]( return await func(self, *args, **kwargs) except AuthenticationError as err: self._entry.async_start_reauth(self._hass) + # pylint: disable-next=home-assistant-exception-not-translated raise BackupAgentError("Authentication error") from err except OneDriveException as err: _LOGGER.error( @@ -89,12 +90,14 @@ def handle_backup_errors[_R, **P]( err, ) _LOGGER.debug("Full error: %s", err, exc_info=True) + # pylint: disable-next=home-assistant-exception-not-translated raise BackupAgentError("Backup operation failed") from err except TimeoutError as err: _LOGGER.error( "Error during backup in %s: Timeout", func.__name__, ) + # pylint: disable-next=home-assistant-exception-not-translated raise BackupAgentError("Backup operation timed out") from err return wrapper @@ -177,6 +180,7 @@ class OneDriveBackupAgent(BackupAgent): ), ) except HashMismatchError as err: + # pylint: disable-next=home-assistant-exception-not-translated raise BackupAgentError( "Hash validation failed, backup file might be corrupt" ) from err @@ -280,4 +284,5 @@ class OneDriveBackupAgent(BackupAgent): if backup := metadata_files.get(backup_id): return backup + # pylint: disable-next=home-assistant-exception-not-translated raise BackupNotFound(f"Backup {backup_id} not found") diff --git a/homeassistant/components/opendisplay/quality_scale.yaml b/homeassistant/components/opendisplay/quality_scale.yaml index 6a14ae56adc..7d488733d2c 100644 --- a/homeassistant/components/opendisplay/quality_scale.yaml +++ b/homeassistant/components/opendisplay/quality_scale.yaml @@ -57,7 +57,7 @@ rules: entity-device-class: done entity-disabled-by-default: done entity-translations: done - exception-translations: done + exception-translations: todo icon-translations: done reconfiguration-flow: status: exempt diff --git a/homeassistant/components/openrgb/coordinator.py b/homeassistant/components/openrgb/coordinator.py index e34710e916b..ecc5c5ff992 100644 --- a/homeassistant/components/openrgb/coordinator.py +++ b/homeassistant/components/openrgb/coordinator.py @@ -64,6 +64,7 @@ class OpenRGBCoordinator(DataUpdateCoordinator[dict[str, Device]]): DEFAULT_CLIENT_NAME, ) except CONNECTION_ERRORS as err: + # pylint: disable-next=home-assistant-exception-translation-key-missing raise UpdateFailed( translation_domain=DOMAIN, translation_key="cannot_connect", diff --git a/homeassistant/components/opower/coordinator.py b/homeassistant/components/opower/coordinator.py index b96575cd663..8364533ea7f 100644 --- a/homeassistant/components/opower/coordinator.py +++ b/homeassistant/components/opower/coordinator.py @@ -111,6 +111,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, OpowerData]]): raise ConfigEntryAuthFailed from err except CannotConnect as err: _LOGGER.error("Error during login: %s", err) + # pylint: disable-next=home-assistant-exception-not-translated raise UpdateFailed(f"Error during login: {err}") from err try: diff --git a/homeassistant/components/palazzetti/quality_scale.yaml b/homeassistant/components/palazzetti/quality_scale.yaml index d4ef278705c..4c1fb890709 100644 --- a/homeassistant/components/palazzetti/quality_scale.yaml +++ b/homeassistant/components/palazzetti/quality_scale.yaml @@ -65,7 +65,7 @@ rules: entity-device-class: done entity-disabled-by-default: todo entity-translations: done - exception-translations: done + exception-translations: todo icon-translations: done reconfiguration-flow: todo repair-issues: diff --git a/homeassistant/components/peblar/__init__.py b/homeassistant/components/peblar/__init__.py index fa354ff367e..105e2b90cc5 100644 --- a/homeassistant/components/peblar/__init__.py +++ b/homeassistant/components/peblar/__init__.py @@ -48,10 +48,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: PeblarConfigEntry) -> bo system_information = await peblar.system_information() api = await peblar.rest_api(enable=True, access_mode=AccessMode.READ_WRITE) except PeblarConnectionError as err: + # pylint: disable-next=home-assistant-exception-not-translated raise ConfigEntryNotReady("Could not connect to Peblar charger") from err except PeblarAuthenticationError as err: raise ConfigEntryAuthFailed from err except PeblarError as err: + # pylint: disable-next=home-assistant-exception-not-translated raise ConfigEntryNotReady( "Unknown error occurred while connecting to Peblar charger" ) from err diff --git a/homeassistant/components/pooldose/__init__.py b/homeassistant/components/pooldose/__init__.py index 4d53dae405f..65ad4c7c4e9 100644 --- a/homeassistant/components/pooldose/__init__.py +++ b/homeassistant/components/pooldose/__init__.py @@ -64,15 +64,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: PooldoseConfigEntry) -> try: client_status = await client.connect() except TimeoutError as err: + # pylint: disable-next=home-assistant-exception-not-translated raise ConfigEntryNotReady( f"Timeout connecting to PoolDose device: {err}" ) from err except (ConnectionError, OSError) as err: + # pylint: disable-next=home-assistant-exception-not-translated raise ConfigEntryNotReady( f"Failed to connect to PoolDose device: {err}" ) from err if client_status != RequestStatus.SUCCESS: + # pylint: disable-next=home-assistant-exception-not-translated raise ConfigEntryNotReady( f"Failed to create PoolDose client while initialization: {client_status}" ) diff --git a/homeassistant/components/pooldose/coordinator.py b/homeassistant/components/pooldose/coordinator.py index 8591fa73aec..29d5af82a40 100644 --- a/homeassistant/components/pooldose/coordinator.py +++ b/homeassistant/components/pooldose/coordinator.py @@ -49,18 +49,22 @@ class PooldoseCoordinator(DataUpdateCoordinator[StructuredValuesDict]): try: status, instant_values = await self.client.instant_values_structured() except TimeoutError as err: + # pylint: disable-next=home-assistant-exception-not-translated raise UpdateFailed( f"Timeout fetching data from PoolDose device: {err}" ) from err except (ConnectionError, OSError) as err: + # pylint: disable-next=home-assistant-exception-not-translated raise UpdateFailed( f"Failed to connect to PoolDose device while fetching data: {err}" ) from err if status != RequestStatus.SUCCESS: + # pylint: disable-next=home-assistant-exception-not-translated raise UpdateFailed(f"API returned status: {status}") if not instant_values: + # pylint: disable-next=home-assistant-exception-not-translated raise UpdateFailed("No data received from API") _LOGGER.debug("Instant values structured: %s", instant_values) diff --git a/homeassistant/components/ptdevices/coordinator.py b/homeassistant/components/ptdevices/coordinator.py index 353918356f9..38e893d8cbe 100644 --- a/homeassistant/components/ptdevices/coordinator.py +++ b/homeassistant/components/ptdevices/coordinator.py @@ -58,12 +58,14 @@ class PTDevicesCoordinator(DataUpdateCoordinator[PTDevicesResponseData]): try: data = await self.interface.get_data() except aioptdevices.PTDevicesRequestError as err: + # pylint: disable-next=home-assistant-exception-placeholder-mismatch raise UpdateFailed( translation_domain=DOMAIN, translation_key="cannot_connect", translation_placeholders={"error": repr(err)}, ) from err except aioptdevices.PTDevicesUnauthorizedError as err: + # pylint: disable-next=home-assistant-exception-placeholder-mismatch raise UpdateFailed( translation_domain=DOMAIN, translation_key="invalid_access_token", diff --git a/homeassistant/components/renault/quality_scale.yaml b/homeassistant/components/renault/quality_scale.yaml index 84a7e352cbc..7a165ca5dec 100644 --- a/homeassistant/components/renault/quality_scale.yaml +++ b/homeassistant/components/renault/quality_scale.yaml @@ -52,7 +52,7 @@ rules: entity-device-class: done entity-disabled-by-default: done entity-translations: done - exception-translations: done + exception-translations: todo icon-translations: done reconfiguration-flow: done repair-issues: done diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index bc1393d955d..23ab5dda23e 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -74,6 +74,7 @@ async def async_setup_entry( await host.async_init() except (UserNotAdmin, CredentialsInvalidError, PasswordIncompatible) as err: await host.stop() + # pylint: disable-next=home-assistant-exception-not-translated raise ConfigEntryAuthFailed(err) from err except ( ReolinkException, diff --git a/homeassistant/components/reolink/coordinator.py b/homeassistant/components/reolink/coordinator.py index 04acfb4f8ec..9bdbce0e01d 100644 --- a/homeassistant/components/reolink/coordinator.py +++ b/homeassistant/components/reolink/coordinator.py @@ -88,12 +88,15 @@ class ReolinkDeviceCoordinator(ReolinkCoordinator): self._host.credential_errors += 1 if self._host.credential_errors >= NUM_CRED_ERRORS: await self._host.stop() + # pylint: disable-next=home-assistant-exception-not-translated raise ConfigEntryAuthFailed(err) from err + # pylint: disable-next=home-assistant-exception-not-translated raise UpdateFailed(str(err)) from err except LoginPrivacyModeError: pass # HTTP API is shutdown when privacy mode is active except ReolinkError as err: self._host.credential_errors = 0 + # pylint: disable-next=home-assistant-exception-not-translated raise UpdateFailed(str(err)) from err self._host.credential_errors = 0 @@ -167,6 +170,7 @@ class ReolinkFirmwareCoordinator(ReolinkCoordinator): ) return + # pylint: disable-next=home-assistant-exception-not-translated raise UpdateFailed( "Error checking Reolink firmware update" f" from {self._host.api.nvr_name}, " diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index 69b4254238f..0604d358df9 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -171,6 +171,7 @@ class ReolinkHost: translation_placeholders={"name": self._config_entry.title}, ) + # pylint: disable-next=home-assistant-exception-not-translated raise PasswordIncompatible( "Reolink password contains incompatible special character or " "is too long, please change the password to only contain characters: " @@ -192,9 +193,11 @@ class ReolinkHost: await self._api.get_host_data() if self._api.mac_address is None: + # pylint: disable-next=home-assistant-exception-not-translated raise ReolinkSetupException("Could not get mac address") if not self._api.is_admin: + # pylint: disable-next=home-assistant-exception-not-translated raise UserNotAdmin( f"User '{self._api.username}' has authorization level " f"'{self._api.user_level}', only admin users can change camera settings" @@ -739,6 +742,7 @@ class ReolinkHost: self._base_url = get_url(self._hass, prefer_external=True) except NoURLAvailableError as err: self.unregister_webhook() + # pylint: disable-next=home-assistant-exception-not-translated raise ReolinkWebhookException( f"Error registering URL for webhook {event_id}: " "HomeAssistant URL is not available" diff --git a/homeassistant/components/reolink/media_source.py b/homeassistant/components/reolink/media_source.py index 9965a38fa4e..3a3ade03bff 100644 --- a/homeassistant/components/reolink/media_source.py +++ b/homeassistant/components/reolink/media_source.py @@ -63,6 +63,7 @@ class ReolinkVODMediaSource(MediaSource): if item.identifier is not None: identifier = item.identifier.split("|", 6) if identifier[0] != "FILE": + # pylint: disable-next=home-assistant-exception-not-translated raise Unresolvable(f"Unknown media item '{item.identifier}'.") _, config_entry_id, channel_str, stream_res, filename, start_time, end_time = ( @@ -172,6 +173,7 @@ class ReolinkVODMediaSource(MediaSource): event, ) + # pylint: disable-next=home-assistant-exception-not-translated raise Unresolvable(f"Unknown media item '{item.identifier}' during browsing.") async def _async_generate_root(self) -> BrowseMediaSource: diff --git a/homeassistant/components/reolink/util.py b/homeassistant/components/reolink/util.py index db9802eff13..3d4e6481937 100644 --- a/homeassistant/components/reolink/util.py +++ b/homeassistant/components/reolink/util.py @@ -62,6 +62,7 @@ def get_host(hass: HomeAssistant, config_entry_id: str) -> ReolinkHost: config_entry_id ) if config_entry is None: + # pylint: disable-next=home-assistant-exception-not-translated raise Unresolvable( f"Could not find Reolink config entry id '{config_entry_id}'." ) diff --git a/homeassistant/components/roborock/__init__.py b/homeassistant/components/roborock/__init__.py index 8aa819b323f..a131176d60e 100644 --- a/homeassistant/components/roborock/__init__.py +++ b/homeassistant/components/roborock/__init__.py @@ -95,6 +95,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: RoborockConfigEntry) -> prefer_cache=False, ) except RoborockInvalidCredentials as err: + # pylint: disable-next=home-assistant-exception-message-with-translation raise ConfigEntryAuthFailed( "Invalid credentials", translation_domain=DOMAIN, @@ -117,6 +118,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: RoborockConfigEntry) -> ) from err except RoborockException as err: _LOGGER.debug("Failed to get Roborock home data: %s", err) + # pylint: disable-next=home-assistant-exception-message-with-translation raise ConfigEntryNotReady( "Failed to get Roborock home data", translation_domain=DOMAIN, @@ -176,6 +178,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: RoborockConfigEntry) -> len(v1_coords) + len(a01_coords) + len(b01_q7_coords) + len(b01_q10_coords) == 0 and enabled_devices ): + # pylint: disable-next=home-assistant-exception-message-with-translation raise ConfigEntryNotReady( "No devices were able to successfully setup", translation_domain=DOMAIN, diff --git a/homeassistant/components/roborock/coordinator.py b/homeassistant/components/roborock/coordinator.py index 62b2bba75b3..113ab6d9869 100644 --- a/homeassistant/components/roborock/coordinator.py +++ b/homeassistant/components/roborock/coordinator.py @@ -161,6 +161,7 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceState | None]): _LOGGER.info("Home discovery skipped while device is busy/cleaning") except RoborockException as err: _LOGGER.debug("Failed to get maps: %s", err) + # pylint: disable-next=home-assistant-exception-placeholder-mismatch raise UpdateFailed( translation_domain=DOMAIN, translation_key="map_failure", diff --git a/homeassistant/components/roborock/quality_scale.yaml b/homeassistant/components/roborock/quality_scale.yaml index f14e191b589..66656c4f8da 100644 --- a/homeassistant/components/roborock/quality_scale.yaml +++ b/homeassistant/components/roborock/quality_scale.yaml @@ -62,7 +62,7 @@ rules: status: exempt comment: There are no noisy entities. entity-translations: done - exception-translations: done + exception-translations: todo icon-translations: todo reconfiguration-flow: todo repair-issues: done diff --git a/homeassistant/components/samsungtv/device_trigger.py b/homeassistant/components/samsungtv/device_trigger.py index d647ab85286..a8ff8095aae 100644 --- a/homeassistant/components/samsungtv/device_trigger.py +++ b/homeassistant/components/samsungtv/device_trigger.py @@ -43,6 +43,7 @@ async def async_validate_trigger_config( device = async_get_device_entry_by_device_id(hass, device_id) async_get_client_by_device_entry(hass, device) except ValueError as err: + # pylint: disable-next=home-assistant-exception-not-translated raise InvalidDeviceAutomationConfig(err) from err return config diff --git a/homeassistant/components/saunum/__init__.py b/homeassistant/components/saunum/__init__.py index db45b86e11e..8dca2541ab1 100644 --- a/homeassistant/components/saunum/__init__.py +++ b/homeassistant/components/saunum/__init__.py @@ -39,6 +39,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: LeilSaunaConfigEntry) -> try: client = await SaunumClient.create(host) except (SaunumConnectionError, SaunumTimeoutError) as exc: + # pylint: disable-next=home-assistant-exception-not-translated raise ConfigEntryNotReady(f"Error connecting to {host}: {exc}") from exc entry.async_on_unload(client.async_close) diff --git a/homeassistant/components/sftp_storage/quality_scale.yaml b/homeassistant/components/sftp_storage/quality_scale.yaml index 1d34426be02..8842a23ff01 100644 --- a/homeassistant/components/sftp_storage/quality_scale.yaml +++ b/homeassistant/components/sftp_storage/quality_scale.yaml @@ -111,7 +111,7 @@ rules: status: exempt comment: | This integration does not have entities. - exception-translations: done + exception-translations: todo icon-translations: status: exempt comment: | diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index 8b4578f0e1f..e412d1f2536 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -401,6 +401,7 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): """Fetch data.""" if self.sleep_period: # Sleeping device, no point polling it, just mark it unavailable + # pylint: disable-next=home-assistant-exception-placeholder-mismatch raise UpdateFailed( translation_domain=DOMAIN, translation_key="update_error_sleeping_device", @@ -670,6 +671,7 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): if self.sleep_period: # Sleeping device, no point polling it, just mark it unavailable + # pylint: disable-next=home-assistant-exception-placeholder-mismatch raise UpdateFailed( translation_domain=DOMAIN, translation_key="update_error_sleeping_device", diff --git a/homeassistant/components/shelly/update.py b/homeassistant/components/shelly/update.py index 3268caf4776..4a5335c1a63 100644 --- a/homeassistant/components/shelly/update.py +++ b/homeassistant/components/shelly/update.py @@ -344,6 +344,7 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity): translation_placeholders={"device": self.coordinator.name}, ) from err except RpcCallError as err: + # pylint: disable-next=home-assistant-exception-placeholder-mismatch raise HomeAssistantError( translation_domain=DOMAIN, translation_key="ota_update_rpc_error", diff --git a/homeassistant/components/sma/coordinator.py b/homeassistant/components/sma/coordinator.py index 1d4c3762e0b..30969da55c4 100644 --- a/homeassistant/components/sma/coordinator.py +++ b/homeassistant/components/sma/coordinator.py @@ -69,12 +69,14 @@ class SMADataUpdateCoordinator(DataUpdateCoordinator[SMACoordinatorData]): SmaConnectionException, ) as err: await self.async_close_sma_session() + # pylint: disable-next=home-assistant-exception-translation-key-missing raise ConfigEntryNotReady( translation_domain=DOMAIN, translation_key="cannot_connect", translation_placeholders={"error": repr(err)}, ) from err except SmaAuthenticationException as err: + # pylint: disable-next=home-assistant-exception-translation-key-missing raise ConfigEntryAuthFailed( translation_domain=DOMAIN, translation_key="invalid_auth", @@ -89,12 +91,14 @@ class SMADataUpdateCoordinator(DataUpdateCoordinator[SMACoordinatorData]): SmaReadException, SmaConnectionException, ) as err: + # pylint: disable-next=home-assistant-exception-translation-key-missing raise UpdateFailed( translation_domain=DOMAIN, translation_key="cannot_connect", translation_placeholders={"error": repr(err)}, ) from err except SmaAuthenticationException as err: + # pylint: disable-next=home-assistant-exception-translation-key-missing raise ConfigEntryAuthFailed( translation_domain=DOMAIN, translation_key="invalid_auth", diff --git a/homeassistant/components/smarla/quality_scale.yaml b/homeassistant/components/smarla/quality_scale.yaml index 7753996a280..5625249a8a6 100644 --- a/homeassistant/components/smarla/quality_scale.yaml +++ b/homeassistant/components/smarla/quality_scale.yaml @@ -48,7 +48,7 @@ rules: entity-device-class: done entity-disabled-by-default: done entity-translations: done - exception-translations: done + exception-translations: todo icon-translations: done reconfiguration-flow: todo repair-issues: todo diff --git a/homeassistant/components/snoo/switch.py b/homeassistant/components/snoo/switch.py index 5fb2c3529ba..ced180e7cf8 100644 --- a/homeassistant/components/snoo/switch.py +++ b/homeassistant/components/snoo/switch.py @@ -80,6 +80,7 @@ class SnooSwitch(SnooDescriptionEntity, SwitchEntity): True, ) except SnooCommandException as err: + # pylint: disable-next=home-assistant-exception-placeholder-mismatch raise HomeAssistantError( translation_domain=DOMAIN, translation_key="switch_on_failed", @@ -96,6 +97,7 @@ class SnooSwitch(SnooDescriptionEntity, SwitchEntity): False, ) except SnooCommandException as err: + # pylint: disable-next=home-assistant-exception-placeholder-mismatch raise HomeAssistantError( translation_domain=DOMAIN, translation_key="switch_off_failed", diff --git a/homeassistant/components/solarlog/coordinator.py b/homeassistant/components/solarlog/coordinator.py index 30f8d49aa65..0b0c2fe6e7f 100644 --- a/homeassistant/components/solarlog/coordinator.py +++ b/homeassistant/components/solarlog/coordinator.py @@ -86,6 +86,7 @@ class SolarLogBasicDataCoordinator(DataUpdateCoordinator[SolarlogData]): translation_key="auth_failed", ) from ex except SolarLogUpdateError as ex: + # pylint: disable-next=home-assistant-exception-translation-key-missing raise UpdateFailed( translation_domain=DOMAIN, translation_key="update_failed", @@ -153,6 +154,7 @@ class SolarLogDeviceDataCoordinator(DataUpdateCoordinator[dict[int, InverterData translation_key="auth_failed", ) from ex except (SolarLogConnectionError, SolarLogUpdateError) as ex: + # pylint: disable-next=home-assistant-exception-translation-key-missing raise UpdateFailed( translation_domain=DOMAIN, translation_key="update_failed", @@ -251,6 +253,7 @@ class SolarLogLongtimeDataCoordinator(DataUpdateCoordinator[EnergyData]): translation_key="auth_failed", ) from ex except (SolarLogConnectionError, SolarLogUpdateError) as ex: + # pylint: disable-next=home-assistant-exception-translation-key-missing raise UpdateFailed( translation_domain=DOMAIN, translation_key="update_failed", diff --git a/homeassistant/components/swiss_public_transport/__init__.py b/homeassistant/components/swiss_public_transport/__init__.py index 1233003f6d4..d564947d22b 100644 --- a/homeassistant/components/swiss_public_transport/__init__.py +++ b/homeassistant/components/swiss_public_transport/__init__.py @@ -86,6 +86,7 @@ async def async_setup_entry( }, ) from e except OpendataTransportError as e: + # pylint: disable-next=home-assistant-exception-placeholder-mismatch raise ConfigEntryError( translation_domain=DOMAIN, translation_key="invalid_data", diff --git a/homeassistant/components/tedee/coordinator.py b/homeassistant/components/tedee/coordinator.py index 961dcf83cb7..4c41bdc8112 100644 --- a/homeassistant/components/tedee/coordinator.py +++ b/homeassistant/components/tedee/coordinator.py @@ -96,6 +96,7 @@ class TedeeApiCoordinator(DataUpdateCoordinator[dict[int, TedeeLock]]): try: await update_fn() except TedeeLocalAuthException as ex: + # pylint: disable-next=home-assistant-exception-translation-key-missing raise ConfigEntryAuthFailed( translation_domain=DOMAIN, translation_key="authentication_failed", diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index 0757f58f85c..2fdd009bcd2 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -916,6 +916,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TelegramBotConfigEntry) try: await bot.get_me() except InvalidToken as err: + # pylint: disable-next=home-assistant-exception-not-translated raise ConfigEntryAuthFailed("Invalid API token for Telegram Bot.") from err except TelegramError as err: raise ConfigEntryNotReady from err diff --git a/homeassistant/components/tesla_fleet/lock.py b/homeassistant/components/tesla_fleet/lock.py index d1ebaaf7f6d..7e51f888bc4 100644 --- a/homeassistant/components/tesla_fleet/lock.py +++ b/homeassistant/components/tesla_fleet/lock.py @@ -86,6 +86,7 @@ class TeslaFleetCableLockEntity(TeslaFleetVehicleEntity, LockEntity): async def async_lock(self, **kwargs: Any) -> None: """Charge cable Lock cannot be manually locked.""" + # pylint: disable-next=home-assistant-exception-message-with-translation raise ServiceValidationError( "Insert cable to lock", translation_domain=DOMAIN, diff --git a/homeassistant/components/teslemetry/coordinator.py b/homeassistant/components/teslemetry/coordinator.py index 08a981b3c07..2d630446acc 100644 --- a/homeassistant/components/teslemetry/coordinator.py +++ b/homeassistant/components/teslemetry/coordinator.py @@ -87,6 +87,7 @@ class TeslemetryMetadataCoordinator(DataUpdateCoordinator[dict[str, Any]]): except (InvalidToken, SubscriptionRequired, LoginRequired) as e: raise ConfigEntryAuthFailed from e except RETRY_EXCEPTIONS as e: + # pylint: disable-next=home-assistant-exception-placeholder-mismatch raise UpdateFailed( translation_domain=DOMAIN, translation_key="update_failed", @@ -94,6 +95,7 @@ class TeslemetryMetadataCoordinator(DataUpdateCoordinator[dict[str, Any]]): retry_after=_get_retry_after(e), ) from e except TeslaFleetError as e: + # pylint: disable-next=home-assistant-exception-placeholder-mismatch raise UpdateFailed( translation_domain=DOMAIN, translation_key="update_failed", diff --git a/homeassistant/components/teslemetry/lock.py b/homeassistant/components/teslemetry/lock.py index 05822a3d632..a92c7bb8e69 100644 --- a/homeassistant/components/teslemetry/lock.py +++ b/homeassistant/components/teslemetry/lock.py @@ -140,6 +140,7 @@ class TeslemetryCableLockEntity(TeslemetryRootEntity, LockEntity): async def async_lock(self, **kwargs: Any) -> None: """Charge cable Lock cannot be manually locked.""" + # pylint: disable-next=home-assistant-exception-message-with-translation raise ServiceValidationError( "Insert cable to lock", translation_domain=DOMAIN, diff --git a/homeassistant/components/tessie/quality_scale.yaml b/homeassistant/components/tessie/quality_scale.yaml index 7433f41a85f..4468baa6c7b 100644 --- a/homeassistant/components/tessie/quality_scale.yaml +++ b/homeassistant/components/tessie/quality_scale.yaml @@ -72,7 +72,7 @@ rules: entity-device-class: done entity-disabled-by-default: done entity-translations: done - exception-translations: done + exception-translations: todo icon-translations: done reconfiguration-flow: todo repair-issues: todo diff --git a/homeassistant/components/transmission/quality_scale.yaml b/homeassistant/components/transmission/quality_scale.yaml index fdf66ba7ce7..7d5c53f0ead 100644 --- a/homeassistant/components/transmission/quality_scale.yaml +++ b/homeassistant/components/transmission/quality_scale.yaml @@ -54,7 +54,7 @@ rules: comment: | Speed sensors change so frequently that disabling by default may be appropriate. entity-translations: done - exception-translations: done + exception-translations: todo icon-translations: done reconfiguration-flow: todo repair-issues: todo diff --git a/homeassistant/components/unifi_access/__init__.py b/homeassistant/components/unifi_access/__init__.py index 9109f48ec2b..a6d2897ee83 100644 --- a/homeassistant/components/unifi_access/__init__.py +++ b/homeassistant/components/unifi_access/__init__.py @@ -51,10 +51,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: UnifiAccessConfigEntry) try: await client.authenticate() except ApiAuthError as err: + # pylint: disable-next=home-assistant-exception-not-translated raise ConfigEntryAuthFailed( f"Authentication failed for UniFi Access at {entry.data[CONF_HOST]}" ) from err except ApiConnectionError as err: + # pylint: disable-next=home-assistant-exception-not-translated raise ConfigEntryNotReady( f"Unable to connect to UniFi Access at {entry.data[CONF_HOST]}" ) from err diff --git a/homeassistant/components/unifi_access/coordinator.py b/homeassistant/components/unifi_access/coordinator.py index 52616998367..20935693a3e 100644 --- a/homeassistant/components/unifi_access/coordinator.py +++ b/homeassistant/components/unifi_access/coordinator.py @@ -197,12 +197,16 @@ class UnifiAccessCoordinator(DataUpdateCoordinator[UnifiAccessData]): self.client.get_emergency_status(), ) except ApiAuthError as err: + # pylint: disable-next=home-assistant-exception-not-translated raise ConfigEntryAuthFailed(f"Authentication failed: {err}") from err except ApiConnectionError as err: + # pylint: disable-next=home-assistant-exception-not-translated raise UpdateFailed(f"Error connecting to API: {err}") from err except ApiError as err: + # pylint: disable-next=home-assistant-exception-not-translated raise UpdateFailed(f"Error communicating with API: {err}") from err except TimeoutError as err: + # pylint: disable-next=home-assistant-exception-not-translated raise UpdateFailed("Timeout communicating with UniFi Access API") from err previous_lock_rules = self.data.door_lock_rules.copy() if self.data else {} diff --git a/homeassistant/components/unifiprotect/__init__.py b/homeassistant/components/unifiprotect/__init__.py index fa4d34b9fbc..5695a97d2cc 100644 --- a/homeassistant/components/unifiprotect/__init__.py +++ b/homeassistant/components/unifiprotect/__init__.py @@ -84,6 +84,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: UFPConfigEntry) -> bool: except NotAuthorized as err: data_service.auth_retries += 1 if data_service.auth_retries > AUTH_RETRIES: + # pylint: disable-next=home-assistant-exception-not-translated raise ConfigEntryAuthFailed(err) from err raise ConfigEntryNotReady from err except (TimeoutError, ClientError, ServerDisconnectedError) as err: diff --git a/homeassistant/components/unifiprotect/media_source.py b/homeassistant/components/unifiprotect/media_source.py index 40a860e8cb8..19558acd771 100644 --- a/homeassistant/components/unifiprotect/media_source.py +++ b/homeassistant/components/unifiprotect/media_source.py @@ -107,7 +107,9 @@ def _get_month_start_end(start: datetime) -> tuple[datetime, datetime]: def _bad_identifier(identifier: str, err: Exception | None = None) -> NoReturn: msg = f"Unexpected identifier: {identifier}" if err is None: + # pylint: disable-next=home-assistant-exception-not-translated raise BrowseError(msg) + # pylint: disable-next=home-assistant-exception-not-translated raise BrowseError(msg) from err @@ -377,6 +379,7 @@ class ProtectMediaSource(MediaSource): _bad_identifier(f"{data.api.bootstrap.nvr.id}:{subtype}:{event_id}", err) if event.start is None or event.end is None: + # pylint: disable-next=home-assistant-exception-not-translated raise BrowseError("Event is still ongoing") return await self._build_event(data, event, thumbnail_only) @@ -787,6 +790,7 @@ class ProtectMediaSource(MediaSource): if camera_id != "all": camera = data.api.bootstrap.cameras.get(camera_id) if camera is None: + # pylint: disable-next=home-assistant-exception-not-translated raise BrowseError(f"Unknown Camera ID: {camera_id}") name = camera.name or camera.market_name or camera.type is_doorbell = camera.feature_flags.is_doorbell diff --git a/homeassistant/components/uptimerobot/__init__.py b/homeassistant/components/uptimerobot/__init__.py index d2c6a6a00d9..96ffbf795d3 100644 --- a/homeassistant/components/uptimerobot/__init__.py +++ b/homeassistant/components/uptimerobot/__init__.py @@ -15,6 +15,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: UptimeRobotConfigEntry) """Set up UptimeRobot from a config entry.""" key: str = entry.data[CONF_API_KEY] if key.startswith(("ur", "m")): + # pylint: disable-next=home-assistant-exception-not-translated raise ConfigEntryAuthFailed( "Wrong API key type detected, use the 'main' API key" ) diff --git a/homeassistant/components/uptimerobot/coordinator.py b/homeassistant/components/uptimerobot/coordinator.py index 0a25d53ef4e..f4666b1a6c4 100644 --- a/homeassistant/components/uptimerobot/coordinator.py +++ b/homeassistant/components/uptimerobot/coordinator.py @@ -48,8 +48,10 @@ class UptimeRobotDataUpdateCoordinator( try: response = await self.api.async_get_monitors() except UptimeRobotAuthenticationException as exception: + # pylint: disable-next=home-assistant-exception-not-translated raise ConfigEntryAuthFailed(exception) from exception except UptimeRobotException as exception: + # pylint: disable-next=home-assistant-exception-not-translated raise UpdateFailed(exception) from exception if TYPE_CHECKING: diff --git a/homeassistant/components/velbus/quality_scale.yaml b/homeassistant/components/velbus/quality_scale.yaml index 0550837aed1..1ebaec8a1b1 100644 --- a/homeassistant/components/velbus/quality_scale.yaml +++ b/homeassistant/components/velbus/quality_scale.yaml @@ -55,7 +55,7 @@ rules: entity-device-class: todo entity-disabled-by-default: done entity-translations: todo - exception-translations: done + exception-translations: todo icon-translations: done reconfiguration-flow: done repair-issues: diff --git a/homeassistant/components/vesync/__init__.py b/homeassistant/components/vesync/__init__.py index 511e5a458de..aa79df43e91 100644 --- a/homeassistant/components/vesync/__init__.py +++ b/homeassistant/components/vesync/__init__.py @@ -64,14 +64,17 @@ async def async_setup_entry( try: await manager.login() except VeSyncLoginError as err: + # pylint: disable-next=home-assistant-exception-translation-key-missing raise ConfigEntryAuthFailed( translation_domain=DOMAIN, translation_key="invalid_auth" ) from err except VeSyncServerError as err: + # pylint: disable-next=home-assistant-exception-translation-key-missing raise ConfigEntryNotReady( translation_domain=DOMAIN, translation_key="server_error" ) from err except VeSyncAPIResponseError as err: + # pylint: disable-next=home-assistant-exception-translation-key-missing raise ConfigEntryNotReady( translation_domain=DOMAIN, translation_key="api_response_error" ) from err diff --git a/homeassistant/components/victron_remote_monitoring/quality_scale.yaml b/homeassistant/components/victron_remote_monitoring/quality_scale.yaml index 7e3f009b868..d884388a496 100644 --- a/homeassistant/components/victron_remote_monitoring/quality_scale.yaml +++ b/homeassistant/components/victron_remote_monitoring/quality_scale.yaml @@ -54,7 +54,7 @@ rules: entity-device-class: done entity-disabled-by-default: done entity-translations: done - exception-translations: done + exception-translations: todo icon-translations: todo reconfiguration-flow: todo repair-issues: todo diff --git a/homeassistant/components/watergate/quality_scale.yaml b/homeassistant/components/watergate/quality_scale.yaml index f2d058f1062..99a8415c622 100644 --- a/homeassistant/components/watergate/quality_scale.yaml +++ b/homeassistant/components/watergate/quality_scale.yaml @@ -63,7 +63,7 @@ rules: entity-device-class: done entity-disabled-by-default: done entity-translations: done - exception-translations: done + exception-translations: todo icon-translations: done reconfiguration-flow: todo repair-issues: todo diff --git a/homeassistant/components/webdav/__init__.py b/homeassistant/components/webdav/__init__.py index 2421873cd9e..45696a086e4 100644 --- a/homeassistant/components/webdav/__init__.py +++ b/homeassistant/components/webdav/__init__.py @@ -48,6 +48,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: WebDavConfigEntry) -> bo # Ensure the backup directory exists if not await async_ensure_path_exists(client, path): + # pylint: disable-next=home-assistant-exception-translation-key-missing raise ConfigEntryNotReady( translation_domain=DOMAIN, translation_key="cannot_access_or_create_backup_path", diff --git a/homeassistant/components/webdav/quality_scale.yaml b/homeassistant/components/webdav/quality_scale.yaml index 59a98e0e747..b37bd739deb 100644 --- a/homeassistant/components/webdav/quality_scale.yaml +++ b/homeassistant/components/webdav/quality_scale.yaml @@ -124,7 +124,7 @@ rules: status: exempt comment: | This integration does not have entities. - exception-translations: done + exception-translations: todo icon-translations: status: exempt comment: | diff --git a/homeassistant/components/webostv/__init__.py b/homeassistant/components/webostv/__init__.py index 5c9634996c6..89f6c298957 100644 --- a/homeassistant/components/webostv/__init__.py +++ b/homeassistant/components/webostv/__init__.py @@ -46,6 +46,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: WebOsTvConfigEntry) -> b try: await client.connect() except WebOsTvPairError as err: + # pylint: disable-next=home-assistant-exception-not-translated raise ConfigEntryAuthFailed(err) from err # If pairing request accepted there will be no error diff --git a/homeassistant/components/webostv/device_trigger.py b/homeassistant/components/webostv/device_trigger.py index 3268d6bc9b1..0085fd5a2cc 100644 --- a/homeassistant/components/webostv/device_trigger.py +++ b/homeassistant/components/webostv/device_trigger.py @@ -42,6 +42,7 @@ async def async_validate_trigger_config( device = async_get_device_entry_by_device_id(hass, device_id) async_get_client_by_device_entry(hass, device) except ValueError as err: + # pylint: disable-next=home-assistant-exception-not-translated raise InvalidDeviceAutomationConfig(err) from err return config diff --git a/homeassistant/components/webostv/notify.py b/homeassistant/components/webostv/notify.py index 2cb61eaa585..2e5b5406e00 100644 --- a/homeassistant/components/webostv/notify.py +++ b/homeassistant/components/webostv/notify.py @@ -48,6 +48,7 @@ class LgWebOSNotificationService(BaseNotificationService): icon_path = data.get(ATTR_ICON) if data else None if not client.tv_state.is_on: + # pylint: disable-next=home-assistant-exception-placeholder-mismatch raise HomeAssistantError( translation_domain=DOMAIN, translation_key="notify_device_off", diff --git a/pylint/plugins/pylint_home_assistant/checkers/exception_translations.py b/pylint/plugins/pylint_home_assistant/checkers/exception_translations.py new file mode 100644 index 00000000000..da60d76eb68 --- /dev/null +++ b/pylint/plugins/pylint_home_assistant/checkers/exception_translations.py @@ -0,0 +1,274 @@ +"""Checker for HomeAssistantError translation usage. + +Ensures that ``HomeAssistantError`` and its subclasses use the translation +system (``translation_domain``, ``translation_key``) instead of hardcoded +English strings. Also verifies that referenced translation keys exist in +the integration's ``strings.json`` and that placeholders match. + +- ``W7417``: Hardcoded string instead of translations (quality-scale-gated) +- ``W7419``: Both a message string and ``translation_key`` provided +- ``E7406``: Translation key not found in ``strings.json`` +- ``E7408``: Only one of ``translation_key``/``translation_domain`` provided +- ``E7418``: Placeholder mismatch between code and ``strings.json`` +""" + +from pathlib import Path + +import astroid +from astroid import nodes +from pylint.checkers import BaseChecker +from pylint.lint import PyLinter + +from pylint_home_assistant.helpers.integration import get_integration_dir +from pylint_home_assistant.helpers.module_info import parse_module +from pylint_home_assistant.helpers.quality_scale import quality_scale_rule_is_done +from pylint_home_assistant.helpers.translations import ( + extract_placeholder_keys, + get_exception_translations, + get_exception_translations_for_domain, + get_message_placeholders, +) + +_HA_ERROR_QNAME = "homeassistant.exceptions.HomeAssistantError" + + +def _accepts_translation_key(class_node: nodes.ClassDef) -> bool: + """Check if a class accepts translation_key in its constructor. + + If the class overrides ``__init__`` without a ``translation_key`` + parameter (and without ``**kwargs``), it doesn't support the + translation system. + """ + for method in class_node.mymethods(): + if method.name != "__init__": + continue + # Class has its own __init__, check if it accepts translation_key + if method.args.kwarg: + return True + return any( + arg.name == "translation_key" + for arg in method.args.args + method.args.kwonlyargs + ) + # No __init__ override, inherits from parent (HomeAssistantError accepts it) + return True + + +def _is_ha_exception(call: nodes.Call) -> str | None: + """Check if a call constructs a HomeAssistantError subclass that supports translations. + + Returns the class name if it is, None otherwise. + """ + try: + for inferred in call.func.infer(): + if not isinstance(inferred, nodes.ClassDef): + continue + is_ha = inferred.qname() == _HA_ERROR_QNAME + if not is_ha: + try: + is_ha = any( + a.qname() == _HA_ERROR_QNAME for a in inferred.ancestors() + ) + except astroid.exceptions.InferenceError: + continue + if is_ha and _accepts_translation_key(inferred): + return str(inferred.name) + except astroid.exceptions.InferenceError: + pass + return None + + +def _get_keyword_value(call: nodes.Call, name: str) -> nodes.NodeNG | None: + """Get the value of a keyword argument from a Call node.""" + for kw in call.keywords: + if kw.arg == name: + return kw.value + return None + + +def _extract_const_string(node: nodes.NodeNG | None) -> str | None: + """Extract a constant string value from a node.""" + if isinstance(node, nodes.Const) and isinstance(node.value, str): + return node.value + return None + + +class ExceptionTranslationsChecker(BaseChecker): + """Checker for HomeAssistantError translation usage.""" + + name = "home_assistant_exception_translations" + priority = -1 + msgs = { + "W7417": ( + "%s should use translation_domain and translation_key", + "home-assistant-exception-not-translated", + "Used when a HomeAssistantError subclass is raised without " + "using the translation system.", + ), + "E7406": ( + "Translation key '%s' not found in exceptions section of " + "strings.json for domain '%s'", + "home-assistant-exception-translation-key-missing", + "Used when a HomeAssistantError references a translation_key " + "that does not exist in the integration's strings.json.", + ), + "W7419": ( + "%s should not pass positional arguments when translation_key is set", + "home-assistant-exception-message-with-translation", + "Used when a HomeAssistantError subclass passes both " + "positional arguments and a translation_key. The translation " + "system generates the message from the key.", + ), + "E7408": ( + "%s must set both translation_key and translation_domain, " + "but only one is provided", + "home-assistant-exception-translation-key-domain-mismatch", + "Used when a HomeAssistantError subclass sets translation_key " + "without translation_domain or vice versa. Both are required " + "for the translation system to generate the exception message.", + ), + "E7418": ( + "Placeholder mismatch for translation key '%s': " + "code passes {%s} but strings.json expects {%s}", + "home-assistant-exception-placeholder-mismatch", + "Used when the translation_placeholders in code don't match " + "the placeholders in the strings.json message template.", + ), + } + options = () + + _in_integration: bool + _module_node: nodes.Module | None + _domain: str | None + _components_dir: Path | None + _exception_translations_done: bool + + def visit_module(self, node: nodes.Module) -> None: + """Load integration context.""" + parsed = parse_module(node.name) + self._in_integration = parsed is not None + self._module_node = node if parsed else None + self._domain = parsed.domain if parsed else None + integration_dir = get_integration_dir(node) if parsed else None + self._components_dir = integration_dir.parent if integration_dir else None + self._exception_translations_done = ( + parsed is not None + and quality_scale_rule_is_done(node, "exception-translations") + ) + + def visit_call(self, node: nodes.Call) -> None: + """Check HomeAssistantError raises for translation usage.""" + if not self._in_integration or self._module_node is None: + return + + # Must be inside a raise statement + if not isinstance(node.parent, nodes.Raise): + return + + exc_name = _is_ha_exception(node) + if exc_name is None: + return + + translation_key_node = _get_keyword_value(node, "translation_key") + has_translation_key = translation_key_node is not None + translation_key = _extract_const_string(translation_key_node) + + # Resolve domain presence + domain_node = _get_keyword_value(node, "translation_domain") + has_translation_domain = domain_node is not None + + # Case 1: Only one of translation_key/translation_domain provided + if has_translation_key != has_translation_domain: + self.add_message( + "home-assistant-exception-translation-key-domain-mismatch", + node=node, + args=(exc_name,), + ) + return + + # Case 2: No translation_key at all (either hardcoded string or bare raise) + # Only enforced when quality scale rule exception-translations is done + if not has_translation_key: + if self._exception_translations_done: + self.add_message( + "home-assistant-exception-not-translated", + node=node, + args=(exc_name,), + ) + return + + # Case 3: Both message and translation_key (message overrides translation) + if node.args and has_translation_key: + self.add_message( + "home-assistant-exception-message-with-translation", + node=node, + args=(exc_name,), + ) + return + + # If no translation key or non-literal key, skip further checks + if translation_key is None: + return + + # Resolve the domain value + translation_domain = _extract_const_string(domain_node) + if translation_domain is None: + # Try resolving DOMAIN constant + if isinstance(domain_node, nodes.Name) and domain_node.name == "DOMAIN": + translation_domain = self._domain + + if translation_domain is None: + # Non-literal domain (variable, attribute), can't check further + return + + # Load translations for the target domain (may differ from current module) + if translation_domain == self._domain: + exception_translations = get_exception_translations(self._module_node) + else: + exception_translations = get_exception_translations_for_domain( + self._module_node, translation_domain + ) + + # Case 3: Check translation key exists + if translation_key not in exception_translations: + self.add_message( + "home-assistant-exception-translation-key-missing", + node=node, + args=(translation_key, translation_domain), + ) + return + + # Case 4: Check placeholder mismatch + entry = exception_translations[translation_key] + message = entry.get("message", "") + expected = get_message_placeholders(message, self._components_dir) + + placeholder_node = _get_keyword_value(node, "translation_placeholders") + + if placeholder_node is None: + if not expected: + # No placeholders expected and none provided + return + # No translation_placeholders keyword but strings.json expects some + code_placeholders: set[str] = set() + else: + code_placeholders_or_none = extract_placeholder_keys(placeholder_node) + if code_placeholders_or_none is None: + # Non-literal (variable, attribute, etc.), can't check + return + code_placeholders = code_placeholders_or_none + + if code_placeholders != expected: + self.add_message( + "home-assistant-exception-placeholder-mismatch", + node=node, + args=( + translation_key, + ", ".join(sorted(code_placeholders)) or "(none)", + ", ".join(sorted(expected)) or "(none)", + ), + ) + + +def register(linter: PyLinter) -> None: + """Register the checker.""" + linter.register_checker(ExceptionTranslationsChecker(linter)) diff --git a/pylint/plugins/pylint_home_assistant/helpers/translations.py b/pylint/plugins/pylint_home_assistant/helpers/translations.py new file mode 100644 index 00000000000..33ee0dcba4b --- /dev/null +++ b/pylint/plugins/pylint_home_assistant/helpers/translations.py @@ -0,0 +1,213 @@ +"""Helpers for reading integration translation files.""" + +import contextlib +from pathlib import Path +import re + +import astroid +from astroid import nodes +import orjson + +from .integration import get_integration_dir + +_InferenceError = astroid.exceptions.InferenceError + +_translations_cache: dict[str, dict | None] = {} + + +def clear_translations_cache() -> None: + """Clear the translations cache (used by tests).""" + _translations_cache.clear() + + +def _load_translations_from_dir(integration_dir: Path) -> dict | None: + """Load translations from an integration directory.""" + cache_key = str(integration_dir) + if cache_key in _translations_cache: + return _translations_cache[cache_key] + + # Core integrations use strings.json, custom integrations use translations/en.json + for candidate in ( + integration_dir / "strings.json", + integration_dir / "translations" / "en.json", + ): + if candidate.exists(): + result: dict | None = None + with contextlib.suppress(orjson.JSONDecodeError, OSError): + parsed = orjson.loads(candidate.read_bytes()) + if isinstance(parsed, dict): + result = parsed + _translations_cache[cache_key] = result + return result + + _translations_cache[cache_key] = None + return None + + +def load_translations(module: nodes.Module) -> dict | None: + """Load and cache the translation data for the current integration. + + For core integrations, reads ``strings.json``. + For custom integrations, reads ``translations/en.json``. + + Returns the parsed JSON as a dict, or ``None`` if not found. + """ + integration_dir = get_integration_dir(module) + if integration_dir is None: + return None + return _load_translations_from_dir(integration_dir) + + +def load_translations_for_domain(module: nodes.Module, domain: str) -> dict | None: + """Load translations for a specific domain. + + Resolves the integration directory for *domain* relative to the + current module's components root. This handles cases where + ``translation_domain`` points to a different integration than the + one currently being linted. + """ + integration_dir = get_integration_dir(module) + if integration_dir is None: + return None + + # Navigate to the sibling integration directory + components_dir = integration_dir.parent + target_dir = components_dir / domain + if not target_dir.is_dir(): + return None + + return _load_translations_from_dir(target_dir) + + +def get_exception_translations(module: nodes.Module) -> dict[str, dict]: + """Return the ``exceptions`` section from the current integration's translations.""" + return _get_exceptions_from_data(load_translations(module)) + + +def get_exception_translations_for_domain( + module: nodes.Module, domain: str +) -> dict[str, dict]: + """Return the ``exceptions`` section for a specific domain.""" + return _get_exceptions_from_data(load_translations_for_domain(module, domain)) + + +def _get_exceptions_from_data(data: dict | None) -> dict[str, dict]: + """Extract the exceptions section from translation data.""" + if data is None: + return {} + exceptions = data.get("exceptions") + if not isinstance(exceptions, dict): + return {} + return exceptions + + +def extract_placeholder_keys(node: nodes.NodeNG | None) -> set[str] | None: + """Extract placeholder key names from a translation_placeholders value. + + Handles inline dict literals directly. For variable references, uses + astroid inference to resolve to the dict definition. + Returns None if the value cannot be resolved. + """ + if node is None: + return None + if isinstance(node, nodes.Dict): + return _keys_from_dict(node) + try: + for inferred in node.infer(): + if isinstance(inferred, nodes.Dict): + return _keys_from_dict(inferred) + except _InferenceError: + pass + return None + + +def _resolve_string_key(key: nodes.NodeNG) -> str | None: + """Resolve a dict key to a string value.""" + if isinstance(key, nodes.Const) and isinstance(key.value, str): + return key.value + # Try inference for constant references (e.g., CONF_DOMAIN) + try: + for inferred in key.infer(): + if isinstance(inferred, nodes.Const) and isinstance(inferred.value, str): + return str(inferred.value) + except _InferenceError: + pass + return None + + +def _keys_from_dict(node: nodes.Dict) -> set[str]: + """Extract string keys from a Dict node. + + Handles literal string keys, constant references (e.g., ``CONF_DOMAIN``) + via astroid inference, and ``**expr`` dict unpacking by inferring the + unpacked expression. + """ + keys: set[str] = set() + for key, value in node.items: + if isinstance(key, nodes.DictUnpack): + # Resolve the unpacked dict to extract its keys + try: + for inferred in value.infer(): + if isinstance(inferred, nodes.Dict): + keys.update(_keys_from_dict(inferred)) + except _InferenceError: + pass + continue + resolved = _resolve_string_key(key) + if resolved is not None: + keys.add(resolved) + return keys + + +_KEY_REF_PATTERN = re.compile(r"^\[%key:(.+)%\]$") + + +def resolve_translation_reference(message: str, components_dir: Path | None) -> str: + """Resolve ``[%key:component::domain::section::key::field%]`` references. + + Returns the resolved message string, or the original if resolution fails. + """ + match = _KEY_REF_PATTERN.match(message) + if not match or components_dir is None: + return message + + parts = match.group(1).split("::") + # Expected: component::domain::section::key::field + # or: common::section::subsection::key + if len(parts) < 3: + return message + + data: dict | None = None + walk_parts: list[str] = [] + + if parts[0] == "component" and len(parts) >= 4: + data = _load_translations_from_dir(components_dir / parts[1]) + walk_parts = parts[2:] + elif parts[0] == "common": + # common:: references live in homeassistant/strings.json + data = _load_translations_from_dir(components_dir.parent) + walk_parts = parts # walk from "common" onwards + else: + return message + + if data is None: + return message + + current: dict | str = data + for part in walk_parts: + if not isinstance(current, dict): + return message + current = current.get(part, message) + return str(current) if isinstance(current, str) else message + + +def get_message_placeholders( + message: str, components_dir: Path | None = None +) -> set[str]: + """Extract placeholder names from a translation message template. + + Resolves ``[%key:...]`` references before extracting placeholders. + Placeholders use Python ``str.format()`` syntax: ``{name}``. + """ + resolved = resolve_translation_reference(message, components_dir) + return set(re.findall(r"\{(\w+)\}", resolved)) diff --git a/tests/pylint/test_exception_translations.py b/tests/pylint/test_exception_translations.py new file mode 100644 index 00000000000..34e282f2398 --- /dev/null +++ b/tests/pylint/test_exception_translations.py @@ -0,0 +1,707 @@ +"""Tests for the exception translations checker.""" + +import json +from pathlib import Path + +import astroid +from pylint.testutils import UnittestLinter +from pylint.utils.ast_walker import ASTWalker +from pylint_home_assistant.checkers.exception_translations import ( + ExceptionTranslationsChecker, +) +from pylint_home_assistant.helpers.quality_scale import clear_quality_scale_cache +from pylint_home_assistant.helpers.translations import clear_translations_cache +import pytest +import yaml + +from . import assert_no_messages + +# Pre-load so astroid can resolve exception classes in parsed snippets. +astroid.MANAGER.ast_from_module_name("homeassistant.exceptions") + +_HA_IMPORTS = ( + "from homeassistant.exceptions import (" + "HomeAssistantError, ServiceValidationError, ConfigEntryAuthFailed)" +) + + +@pytest.fixture(name="translations_checker") +def translations_checker_fixture( + linter: UnittestLinter, +) -> ExceptionTranslationsChecker: + """Fixture to provide an exception translations checker.""" + clear_translations_cache() + clear_quality_scale_cache() + return ExceptionTranslationsChecker(linter) + + +def _make_integration( + tmp_path: Path, + exceptions: dict | None = None, + *, + exception_translations_done: bool = False, +) -> Path: + """Create a fake integration with optional strings.json.""" + integration_dir = tmp_path / "homeassistant" / "components" / "test_int" + integration_dir.mkdir(parents=True) + if exceptions is not None: + strings = {"exceptions": exceptions} + (integration_dir / "strings.json").write_text(json.dumps(strings)) + if exception_translations_done: + qs = {"rules": {"exception-translations": "done"}} + (integration_dir / "quality_scale.yaml").write_text(yaml.dump(qs)) + return integration_dir + + +@pytest.mark.parametrize( + "code", + [ + pytest.param( + f""" +{_HA_IMPORTS} +raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="some_error", +) +""", + id="translated_no_placeholders", + ), + pytest.param( + f""" +{_HA_IMPORTS} +raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="error_with_name", + translation_placeholders={{"name": device_name}}, +) +""", + id="translated_with_placeholders", + ), + pytest.param( + f""" +{_HA_IMPORTS} +raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="invalid_input", +) +""", + id="service_validation_error_translated", + ), + pytest.param( + f""" +{_HA_IMPORTS} +raise ConfigEntryAuthFailed( + translation_domain=DOMAIN, + translation_key="invalid_api_key", +) +""", + id="config_entry_auth_failed_translated", + ), + pytest.param( + f""" +{_HA_IMPORTS} +raise HomeAssistantError() +""", + id="no_args_no_translation", + ), + ], +) +def test_no_warning( + linter: UnittestLinter, + translations_checker: ExceptionTranslationsChecker, + tmp_path: Path, + code: str, +) -> None: + """Test cases that should not trigger a warning.""" + integration_dir = _make_integration( + tmp_path, + exceptions={ + "some_error": {"message": "An error occurred"}, + "error_with_name": {"message": "Error for {name}"}, + "invalid_input": {"message": "Invalid input"}, + "invalid_api_key": {"message": "Invalid API key"}, + }, + ) + root_node = astroid.parse(code, "homeassistant.components.test_int.coordinator") + root_node.file = str(integration_dir / "coordinator.py") + + walker = ASTWalker(linter) + walker.add_checker(translations_checker) + + with assert_no_messages(linter): + walker.walk(root_node) + + +@pytest.mark.parametrize( + ("exc_class", "code"), + [ + pytest.param( + "HomeAssistantError", + f'{_HA_IMPORTS}\nraise HomeAssistantError("Something went wrong")', + id="ha_error_hardcoded", + ), + pytest.param( + "ServiceValidationError", + f'{_HA_IMPORTS}\nraise ServiceValidationError("Invalid value")', + id="service_validation_hardcoded", + ), + pytest.param( + "ConfigEntryAuthFailed", + f'{_HA_IMPORTS}\nraise ConfigEntryAuthFailed("Bad credentials")', + id="auth_failed_hardcoded", + ), + ], +) +def test_hardcoded_string_flagged( + linter: UnittestLinter, + translations_checker: ExceptionTranslationsChecker, + tmp_path: Path, + exc_class: str, + code: str, +) -> None: + """Test that hardcoded string exceptions are flagged when quality scale rule is done.""" + integration_dir = _make_integration(tmp_path, exception_translations_done=True) + root_node = astroid.parse(code, "homeassistant.components.test_int.coordinator") + root_node.file = str(integration_dir / "coordinator.py") + + walker = ASTWalker(linter) + walker.add_checker(translations_checker) + walker.walk(root_node) + + messages = linter.release_messages() + assert len(messages) == 1 + assert messages[0].msg_id == "home-assistant-exception-not-translated" + assert exc_class in messages[0].args[0] + + +@pytest.mark.parametrize( + "code", + [ + pytest.param( + f""" +{_HA_IMPORTS} +raise HomeAssistantError( + translation_key="some_error", +) +""", + id="key_without_domain", + ), + pytest.param( + f""" +{_HA_IMPORTS} +raise HomeAssistantError( + translation_domain=DOMAIN, +) +""", + id="domain_without_key", + ), + pytest.param( + f""" +{_HA_IMPORTS} +raise HomeAssistantError("Something failed", translation_domain=DOMAIN) +""", + id="hardcoded_with_domain_no_key", + ), + ], +) +def test_translation_key_domain_mismatch_flagged( + linter: UnittestLinter, + translations_checker: ExceptionTranslationsChecker, + tmp_path: Path, + code: str, +) -> None: + """Test that translation_key without domain or domain without key is flagged.""" + integration_dir = _make_integration(tmp_path) + root_node = astroid.parse( + code, + "homeassistant.components.test_int.coordinator", + ) + root_node.file = str(integration_dir / "coordinator.py") + + walker = ASTWalker(linter) + walker.add_checker(translations_checker) + walker.walk(root_node) + + messages = linter.release_messages() + assert len(messages) == 1 + assert ( + messages[0].msg_id == "home-assistant-exception-translation-key-domain-mismatch" + ) + + +def test_message_with_translation_key_flagged( + linter: UnittestLinter, + translations_checker: ExceptionTranslationsChecker, + tmp_path: Path, +) -> None: + """Test that passing both a message and translation_key is flagged.""" + integration_dir = _make_integration(tmp_path) + root_node = astroid.parse( + f""" +{_HA_IMPORTS} +raise HomeAssistantError( + "This should not be here", + translation_domain=DOMAIN, + translation_key="some_error", +) +""", + "homeassistant.components.test_int.coordinator", + ) + root_node.file = str(integration_dir / "coordinator.py") + + walker = ASTWalker(linter) + walker.add_checker(translations_checker) + walker.walk(root_node) + + messages = linter.release_messages() + assert len(messages) == 1 + assert messages[0].msg_id == "home-assistant-exception-message-with-translation" + + +def test_missing_translation_key_flagged( + linter: UnittestLinter, + translations_checker: ExceptionTranslationsChecker, + tmp_path: Path, +) -> None: + """Test that a missing translation key in strings.json is flagged.""" + integration_dir = _make_integration( + tmp_path, + exceptions={"existing_key": {"message": "This exists"}}, + ) + root_node = astroid.parse( + f""" +{_HA_IMPORTS} +raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="nonexistent_key", +) +""", + "homeassistant.components.test_int.coordinator", + ) + root_node.file = str(integration_dir / "coordinator.py") + + walker = ASTWalker(linter) + walker.add_checker(translations_checker) + walker.walk(root_node) + + messages = linter.release_messages() + assert len(messages) == 1 + assert messages[0].msg_id == "home-assistant-exception-translation-key-missing" + assert "nonexistent_key" in messages[0].args[0] + + +def test_existing_translation_key_ok( + linter: UnittestLinter, + translations_checker: ExceptionTranslationsChecker, + tmp_path: Path, +) -> None: + """Test that an existing translation key passes.""" + integration_dir = _make_integration( + tmp_path, + exceptions={"some_error": {"message": "An error occurred"}}, + ) + root_node = astroid.parse( + f""" +{_HA_IMPORTS} +raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="some_error", +) +""", + "homeassistant.components.test_int.coordinator", + ) + root_node.file = str(integration_dir / "coordinator.py") + + walker = ASTWalker(linter) + walker.add_checker(translations_checker) + + with assert_no_messages(linter): + walker.walk(root_node) + + +def test_extra_placeholders_flagged( + linter: UnittestLinter, + translations_checker: ExceptionTranslationsChecker, + tmp_path: Path, +) -> None: + """Test that extra placeholders are flagged when strings.json expects none.""" + integration_dir = _make_integration( + tmp_path, + exceptions={ + "some_error": {"message": "Something failed"}, + }, + ) + root_node = astroid.parse( + f""" +{_HA_IMPORTS} +raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="some_error", + translation_placeholders={{"extra": "value"}}, +) +""", + "homeassistant.components.test_int.coordinator", + ) + root_node.file = str(integration_dir / "coordinator.py") + + walker = ASTWalker(linter) + walker.add_checker(translations_checker) + walker.walk(root_node) + + messages = linter.release_messages() + assert len(messages) == 1 + assert messages[0].msg_id == "home-assistant-exception-placeholder-mismatch" + + +def test_placeholder_mismatch_flagged( + linter: UnittestLinter, + translations_checker: ExceptionTranslationsChecker, + tmp_path: Path, +) -> None: + """Test that placeholder mismatches are flagged.""" + integration_dir = _make_integration( + tmp_path, + exceptions={ + "some_error": {"message": "Error for {device_name}: {error}"}, + }, + ) + root_node = astroid.parse( + f""" +{_HA_IMPORTS} +raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="some_error", + translation_placeholders={{"device_name": name, "wrong_key": "value"}}, +) +""", + "homeassistant.components.test_int.coordinator", + ) + root_node.file = str(integration_dir / "coordinator.py") + + walker = ASTWalker(linter) + walker.add_checker(translations_checker) + walker.walk(root_node) + + messages = linter.release_messages() + assert len(messages) == 1 + assert messages[0].msg_id == "home-assistant-exception-placeholder-mismatch" + + +def test_placeholder_match_ok( + linter: UnittestLinter, + translations_checker: ExceptionTranslationsChecker, + tmp_path: Path, +) -> None: + """Test that matching placeholders pass.""" + integration_dir = _make_integration( + tmp_path, + exceptions={ + "some_error": {"message": "Error for {device_name}: {error}"}, + }, + ) + root_node = astroid.parse( + f""" +{_HA_IMPORTS} +raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="some_error", + translation_placeholders={{"device_name": name, "error": str(err)}}, +) +""", + "homeassistant.components.test_int.coordinator", + ) + root_node.file = str(integration_dir / "coordinator.py") + + walker = ASTWalker(linter) + walker.add_checker(translations_checker) + + with assert_no_messages(linter): + walker.walk(root_node) + + +def test_placeholder_variable_resolved( + linter: UnittestLinter, + translations_checker: ExceptionTranslationsChecker, + tmp_path: Path, +) -> None: + """Test that placeholders via a variable are resolved through inference.""" + integration_dir = _make_integration( + tmp_path, + exceptions={ + "some_error": {"message": "Error for {device_name}: {error}"}, + }, + ) + root_node = astroid.parse( + f""" +{_HA_IMPORTS} +placeholders = {{"device_name": name, "error": str(err)}} +raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="some_error", + translation_placeholders=placeholders, +) +""", + "homeassistant.components.test_int.coordinator", + ) + root_node.file = str(integration_dir / "coordinator.py") + + walker = ASTWalker(linter) + walker.add_checker(translations_checker) + + with assert_no_messages(linter): + walker.walk(root_node) + + +def test_placeholder_variable_mismatch_flagged( + linter: UnittestLinter, + translations_checker: ExceptionTranslationsChecker, + tmp_path: Path, +) -> None: + """Test that a variable with wrong placeholder keys is still flagged.""" + integration_dir = _make_integration( + tmp_path, + exceptions={ + "some_error": {"message": "Error for {device_name}: {error}"}, + }, + ) + root_node = astroid.parse( + f""" +{_HA_IMPORTS} +placeholders = {{"wrong": name}} +raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="some_error", + translation_placeholders=placeholders, +) +""", + "homeassistant.components.test_int.coordinator", + ) + root_node.file = str(integration_dir / "coordinator.py") + + walker = ASTWalker(linter) + walker.add_checker(translations_checker) + walker.walk(root_node) + + messages = linter.release_messages() + assert len(messages) == 1 + assert messages[0].msg_id == "home-assistant-exception-placeholder-mismatch" + + +def test_dict_unpacking_placeholders_ok( + linter: UnittestLinter, + translations_checker: ExceptionTranslationsChecker, + tmp_path: Path, +) -> None: + """Test that **dict unpacking is resolved for placeholder validation.""" + integration_dir = _make_integration( + tmp_path, + exceptions={ + "some_error": {"message": "Error for {name}: {reason}"}, + }, + ) + root_node = astroid.parse( + f""" +{_HA_IMPORTS} +base = {{"name": device_name}} +raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="some_error", + translation_placeholders={{**base, "reason": str(err)}}, +) +""", + "homeassistant.components.test_int.coordinator", + ) + root_node.file = str(integration_dir / "coordinator.py") + + walker = ASTWalker(linter) + walker.add_checker(translations_checker) + + with assert_no_messages(linter): + walker.walk(root_node) + + +def test_constant_placeholder_keys_ok( + linter: UnittestLinter, + translations_checker: ExceptionTranslationsChecker, + tmp_path: Path, +) -> None: + """Test that constant keys in placeholder dicts are resolved.""" + integration_dir = _make_integration( + tmp_path, + exceptions={ + "some_error": {"message": "Error for {name}"}, + }, + ) + root_node = astroid.parse( + f""" +{_HA_IMPORTS} +ATTR_NAME = "name" +raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="some_error", + translation_placeholders={{ATTR_NAME: device_name}}, +) +""", + "homeassistant.components.test_int.coordinator", + ) + root_node.file = str(integration_dir / "coordinator.py") + + walker = ASTWalker(linter) + walker.add_checker(translations_checker) + + with assert_no_messages(linter): + walker.walk(root_node) + + +def test_key_reference_resolution( + linter: UnittestLinter, + translations_checker: ExceptionTranslationsChecker, + tmp_path: Path, +) -> None: + """Test that [%key:component::...%] references are resolved for placeholders.""" + # Create the referenced integration + ref_dir = tmp_path / "homeassistant" / "components" / "other_int" + ref_dir.mkdir(parents=True) + (ref_dir / "strings.json").write_text( + json.dumps({"exceptions": {"ref_error": {"message": "Error for {device}"}}}) + ) + + # Create the integration that references it + integration_dir = _make_integration( + tmp_path, + exceptions={ + "some_error": { + "message": "[%key:component::other_int::exceptions::ref_error::message%]" + }, + }, + ) + root_node = astroid.parse( + f""" +{_HA_IMPORTS} +raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="some_error", + translation_placeholders={{"device": device_name}}, +) +""", + "homeassistant.components.test_int.coordinator", + ) + root_node.file = str(integration_dir / "coordinator.py") + + walker = ASTWalker(linter) + walker.add_checker(translations_checker) + + with assert_no_messages(linter): + walker.walk(root_node) + + +def test_no_strings_json_flags_missing_key( + linter: UnittestLinter, + translations_checker: ExceptionTranslationsChecker, + tmp_path: Path, +) -> None: + """Test that a translation_key is flagged when no strings.json exists.""" + integration_dir = _make_integration(tmp_path) + root_node = astroid.parse( + f""" +{_HA_IMPORTS} +raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="some_error", +) +""", + "homeassistant.components.test_int.coordinator", + ) + root_node.file = str(integration_dir / "coordinator.py") + + walker = ASTWalker(linter) + walker.add_checker(translations_checker) + walker.walk(root_node) + + messages = linter.release_messages() + assert len(messages) == 1 + assert messages[0].msg_id == "home-assistant-exception-translation-key-missing" + + +def test_missing_placeholders_flagged( + linter: UnittestLinter, + translations_checker: ExceptionTranslationsChecker, + tmp_path: Path, +) -> None: + """Test that missing translation_placeholders are flagged when strings.json expects them.""" + integration_dir = _make_integration( + tmp_path, + exceptions={ + "some_error": {"message": "Error for {device_name}: {error}"}, + }, + ) + root_node = astroid.parse( + f""" +{_HA_IMPORTS} +raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="some_error", +) +""", + "homeassistant.components.test_int.coordinator", + ) + root_node.file = str(integration_dir / "coordinator.py") + + walker = ASTWalker(linter) + walker.add_checker(translations_checker) + walker.walk(root_node) + + messages = linter.release_messages() + assert len(messages) == 1 + assert messages[0].msg_id == "home-assistant-exception-placeholder-mismatch" + + +def test_custom_integration_en_json( + linter: UnittestLinter, + translations_checker: ExceptionTranslationsChecker, + tmp_path: Path, +) -> None: + """Test that custom integrations use translations/en.json.""" + integration_dir = tmp_path / "homeassistant" / "components" / "test_int" + integration_dir.mkdir(parents=True) + translations_dir = integration_dir / "translations" + translations_dir.mkdir() + (translations_dir / "en.json").write_text( + json.dumps({"exceptions": {"my_error": {"message": "It broke"}}}) + ) + + root_node = astroid.parse( + f""" +{_HA_IMPORTS} +raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="missing_key", +) +""", + "homeassistant.components.test_int.coordinator", + ) + root_node.file = str(integration_dir / "coordinator.py") + + walker = ASTWalker(linter) + walker.add_checker(translations_checker) + walker.walk(root_node) + + messages = linter.release_messages() + assert len(messages) == 1 + assert messages[0].msg_id == "home-assistant-exception-translation-key-missing" + + +def test_not_integration_ignored( + linter: UnittestLinter, + translations_checker: ExceptionTranslationsChecker, +) -> None: + """Test that non-integration modules are ignored.""" + root_node = astroid.parse( + f'{_HA_IMPORTS}\nraise HomeAssistantError("hardcoded")', + "tests.components.test_integration", + ) + walker = ASTWalker(linter) + walker.add_checker(translations_checker) + + with assert_no_messages(linter): + walker.walk(root_node)