|  | 
|  | 1 | +"""The Backblaze B2 integration.""" | 
|  | 2 | + | 
|  | 3 | +from __future__ import annotations | 
|  | 4 | + | 
|  | 5 | +from datetime import timedelta | 
|  | 6 | +import logging | 
|  | 7 | +from typing import Any | 
|  | 8 | + | 
|  | 9 | +from b2sdk.v2 import B2Api, Bucket, InMemoryAccountInfo, exception | 
|  | 10 | + | 
|  | 11 | +from homeassistant.config_entries import ConfigEntry | 
|  | 12 | +from homeassistant.core import HomeAssistant | 
|  | 13 | +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady | 
|  | 14 | +from homeassistant.helpers.event import async_track_time_interval | 
|  | 15 | + | 
|  | 16 | +from .const import ( | 
|  | 17 | +    BACKBLAZE_REALM, | 
|  | 18 | +    CONF_APPLICATION_KEY, | 
|  | 19 | +    CONF_BUCKET, | 
|  | 20 | +    CONF_KEY_ID, | 
|  | 21 | +    DATA_BACKUP_AGENT_LISTENERS, | 
|  | 22 | +    DOMAIN, | 
|  | 23 | +) | 
|  | 24 | +from .repairs import ( | 
|  | 25 | +    async_check_for_repair_issues, | 
|  | 26 | +    create_bucket_access_restricted_issue, | 
|  | 27 | +    create_bucket_not_found_issue, | 
|  | 28 | +) | 
|  | 29 | + | 
|  | 30 | +_LOGGER = logging.getLogger(__name__) | 
|  | 31 | + | 
|  | 32 | +type BackblazeConfigEntry = ConfigEntry[Bucket] | 
|  | 33 | + | 
|  | 34 | + | 
|  | 35 | +async def async_setup_entry(hass: HomeAssistant, entry: BackblazeConfigEntry) -> bool: | 
|  | 36 | +    """Set up Backblaze B2 from a config entry.""" | 
|  | 37 | + | 
|  | 38 | +    info = InMemoryAccountInfo() | 
|  | 39 | +    b2_api = B2Api(info) | 
|  | 40 | + | 
|  | 41 | +    def _authorize_and_get_bucket_sync() -> Bucket: | 
|  | 42 | +        """Synchronously authorize the Backblaze B2 account and retrieve the bucket. | 
|  | 43 | +
 | 
|  | 44 | +        This function runs in the event loop's executor as b2sdk operations are blocking. | 
|  | 45 | +        """ | 
|  | 46 | +        b2_api.authorize_account( | 
|  | 47 | +            BACKBLAZE_REALM, | 
|  | 48 | +            entry.data[CONF_KEY_ID], | 
|  | 49 | +            entry.data[CONF_APPLICATION_KEY], | 
|  | 50 | +        ) | 
|  | 51 | +        return b2_api.get_bucket_by_name(entry.data[CONF_BUCKET]) | 
|  | 52 | + | 
|  | 53 | +    try: | 
|  | 54 | +        bucket = await hass.async_add_executor_job(_authorize_and_get_bucket_sync) | 
|  | 55 | +    except exception.Unauthorized as err: | 
|  | 56 | +        raise ConfigEntryAuthFailed( | 
|  | 57 | +            translation_domain=DOMAIN, | 
|  | 58 | +            translation_key="invalid_credentials", | 
|  | 59 | +        ) from err | 
|  | 60 | +    except exception.RestrictedBucket as err: | 
|  | 61 | +        create_bucket_access_restricted_issue(hass, entry, err.bucket_name) | 
|  | 62 | +        raise ConfigEntryNotReady( | 
|  | 63 | +            translation_domain=DOMAIN, | 
|  | 64 | +            translation_key="restricted_bucket", | 
|  | 65 | +            translation_placeholders={ | 
|  | 66 | +                "restricted_bucket_name": err.bucket_name, | 
|  | 67 | +            }, | 
|  | 68 | +        ) from err | 
|  | 69 | +    except exception.NonExistentBucket as err: | 
|  | 70 | +        create_bucket_not_found_issue(hass, entry, entry.data[CONF_BUCKET]) | 
|  | 71 | +        raise ConfigEntryNotReady( | 
|  | 72 | +            translation_domain=DOMAIN, | 
|  | 73 | +            translation_key="invalid_bucket_name", | 
|  | 74 | +        ) from err | 
|  | 75 | +    except exception.ConnectionReset as err: | 
|  | 76 | +        raise ConfigEntryNotReady( | 
|  | 77 | +            translation_domain=DOMAIN, | 
|  | 78 | +            translation_key="cannot_connect", | 
|  | 79 | +        ) from err | 
|  | 80 | +    except exception.MissingAccountData as err: | 
|  | 81 | +        raise ConfigEntryAuthFailed( | 
|  | 82 | +            translation_domain=DOMAIN, | 
|  | 83 | +            translation_key="invalid_auth", | 
|  | 84 | +        ) from err | 
|  | 85 | + | 
|  | 86 | +    entry.runtime_data = bucket | 
|  | 87 | + | 
|  | 88 | +    def _async_notify_backup_listeners() -> None: | 
|  | 89 | +        """Notify any registered backup agent listeners.""" | 
|  | 90 | +        _LOGGER.debug("Notifying backup listeners for entry %s", entry.entry_id) | 
|  | 91 | +        for listener in hass.data.get(DATA_BACKUP_AGENT_LISTENERS, []): | 
|  | 92 | +            listener() | 
|  | 93 | + | 
|  | 94 | +    entry.async_on_unload(entry.async_on_state_change(_async_notify_backup_listeners)) | 
|  | 95 | + | 
|  | 96 | +    async def _periodic_issue_check(_now: Any) -> None: | 
|  | 97 | +        """Periodically check for repair issues.""" | 
|  | 98 | +        await async_check_for_repair_issues(hass, entry) | 
|  | 99 | + | 
|  | 100 | +    entry.async_on_unload( | 
|  | 101 | +        async_track_time_interval(hass, _periodic_issue_check, timedelta(minutes=30)) | 
|  | 102 | +    ) | 
|  | 103 | + | 
|  | 104 | +    hass.async_create_task(async_check_for_repair_issues(hass, entry)) | 
|  | 105 | + | 
|  | 106 | +    return True | 
|  | 107 | + | 
|  | 108 | + | 
|  | 109 | +async def async_unload_entry(hass: HomeAssistant, entry: BackblazeConfigEntry) -> bool: | 
|  | 110 | +    """Unload a Backblaze B2 config entry. | 
|  | 111 | +
 | 
|  | 112 | +    Any resources directly managed by this entry that need explicit shutdown | 
|  | 113 | +    would be handled here. In this case, the `async_on_state_change` listener | 
|  | 114 | +    handles the notification logic on unload. | 
|  | 115 | +    """ | 
|  | 116 | +    return True | 
0 commit comments