| 
 | 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