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