Skip to content
18 changes: 13 additions & 5 deletions homeassistant/components/google_drive/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from google_drive_api.exceptions import GoogleDriveApiError

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import instance_id
Expand All @@ -19,13 +19,13 @@

from .api import AsyncConfigEntryAuth, DriveClient
from .const import DOMAIN
from .coordinator import GoogleDriveConfigEntry, GoogleDriveDataUpdateCoordinator

DATA_BACKUP_AGENT_LISTENERS: HassKey[list[Callable[[], None]]] = HassKey(
f"{DOMAIN}.backup_agent_listeners"
)


type GoogleDriveConfigEntry = ConfigEntry[DriveClient]
_PLATFORMS = (Platform.SENSOR,)


async def async_setup_entry(hass: HomeAssistant, entry: GoogleDriveConfigEntry) -> bool:
Expand All @@ -41,11 +41,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: GoogleDriveConfigEntry)
await auth.async_get_access_token()

client = DriveClient(await instance_id.async_get(hass), auth)
entry.runtime_data = client

# Test we can access Google Drive and raise if not
try:
await client.async_create_ha_root_folder_if_not_exists()
folder_id, _ = await client.async_create_ha_root_folder_if_not_exists()
except GoogleDriveApiError as err:
raise ConfigEntryNotReady from err

Expand All @@ -55,11 +54,20 @@ def async_notify_backup_listeners() -> None:

entry.async_on_unload(entry.async_on_state_change(async_notify_backup_listeners))

entry.runtime_data = GoogleDriveDataUpdateCoordinator(
hass, entry=entry, client=client, backup_folder_id=folder_id
)
await entry.runtime_data.async_config_entry_first_refresh()

await hass.config_entries.async_forward_entry_setups(entry, _PLATFORMS)

return True


async def async_unload_entry(
hass: HomeAssistant, entry: GoogleDriveConfigEntry
) -> bool:
"""Unload a config entry."""
await hass.config_entries.async_unload_platforms(entry, _PLATFORMS)

return True
24 changes: 24 additions & 0 deletions homeassistant/components/google_drive/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

from collections.abc import AsyncIterator, Callable, Coroutine
from dataclasses import dataclass
import json
import logging
from typing import Any
Expand All @@ -27,6 +28,16 @@
_LOGGER = logging.getLogger(__name__)


@dataclass
class StorageQuotaData:
"""Class to represent storage quota data."""

limit: int | None
usage: int
usage_in_drive: int
usage_in_trash: int


class AsyncConfigEntryAuth(AbstractAuth):
"""Provide Google Drive authentication tied to an OAuth2 based config entry."""

Expand Down Expand Up @@ -95,6 +106,19 @@ async def async_get_email_address(self) -> str:
res = await self._api.get_user(params={"fields": "user(emailAddress)"})
return str(res["user"]["emailAddress"])

async def async_get_storage_quota(self) -> StorageQuotaData:
"""Get storage quota of the current user."""
res = await self._api.get_user(params={"fields": "storageQuota"})

storageQuota = res["storageQuota"]
limit = storageQuota.get("limit")
return StorageQuotaData(
limit=int(limit) if limit is not None else None,
usage=int(storageQuota.get("usage", 0)),
usage_in_drive=int(storageQuota.get("usageInDrive", 0)),
usage_in_trash=int(storageQuota.get("usageInTrash", 0)),
)

async def async_create_ha_root_folder_if_not_exists(self) -> tuple[str, str]:
"""Create Home Assistant folder if it doesn't exist."""
fields = "id,name"
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/google_drive/backup.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def __init__(self, config_entry: GoogleDriveConfigEntry) -> None:
assert config_entry.unique_id
self.name = config_entry.title
self.unique_id = slugify(config_entry.unique_id)
self._client = config_entry.runtime_data
self._client = config_entry.runtime_data.client

async def async_upload_backup(
self,
Expand Down
3 changes: 1 addition & 2 deletions homeassistant/components/google_drive/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,9 @@
from homeassistant.helpers.aiohttp_client import async_get_clientsession

from .api import AsyncConfigFlowAuth, DriveClient
from .const import DOMAIN
from .const import DOMAIN, DRIVE_FOLDER_URL_PREFIX

DEFAULT_NAME = "Google Drive"
DRIVE_FOLDER_URL_PREFIX = "https://drive.google.com/drive/folders/"
OAUTH2_SCOPES = [
"https://www.googleapis.com/auth/drive.file",
]
Expand Down
5 changes: 5 additions & 0 deletions homeassistant/components/google_drive/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,9 @@

from __future__ import annotations

from datetime import timedelta

DOMAIN = "google_drive"

SCAN_INTERVAL = timedelta(minutes=5)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems too frequent. The sensors are only expected to be updated after a backup (typically daily or weekly) or after a manual delete (uncommon). How about going with every few hours? In an earlier commit you had every 6h which seems more reasonable. Whoever wants more frequent can call the homeassistant.update_entity action.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This Google Drive account may not be only used to store HA backups. Other interactions are likely to happen more frequently and should be taken into account.
As long as we're not running into rate-limiting issues I'd say a 5 minute interval is fine. (Or at the very least keep the default more often than hourly).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find a 5 minute interval too excessive. Sure other uploads outside HA could increase your quota usage but do regular users care to track it within minutes? The example in the docs notifies you when you are low on storage. Every few hours is more than enough. Whoever wants more frequently can call homeassistant.update_entity in an automation.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're merely sharing opinion, which is fine, but other users may not share the same view. If rate limits are not an issue on the API call having an extremely low polling rate should not be chosen.

Requiring users to take convoluted steps of implementing an automation to trigger polling at a different rate for any integration where no sensible default was chosen is just the wrong way of going about this.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We generally avoid aggressive polling intervals (like 5 minutes) for data that is not critical for immediate, real-time automation (such as a motion sensor or light switch). Storage quota is a "slow-moving" metric.

The homeassistant.update_entity action is the documented, standard mechanism for the minority of users who require non-standard polling frequencies; it is not a workaround. See e.g. https://www.home-assistant.io/integrations/speedtestdotnet/

@mik-laj Please update the interval to proceed. The shortest I'd accept is 1h.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Six hours sounds good to me. Most users perform one backup per day, so if someone monitors free space to perform backups, everything will work because we'll have notifications four times more often than backups are performed.

This integration does not have services that allow us to create files on Google, so we do not have to worry about other changes on Google Drive.

If someone wants to monitor disk space, they can manually refresh the sensor, but I also don't think that's a common use case to justify changing the interval for all users. The main purpose of this integration is to provide backups, and for that use case, 6 hours is adequate.

DRIVE_FOLDER_URL_PREFIX = "https://drive.google.com/drive/folders/"
77 changes: 77 additions & 0 deletions homeassistant/components/google_drive/coordinator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""DataUpdateCoordinator for Google Drive."""

from __future__ import annotations

from dataclasses import dataclass
import logging

from google_drive_api.exceptions import GoogleDriveApiError

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed

from .api import DriveClient, StorageQuotaData
from .const import DOMAIN, SCAN_INTERVAL

type GoogleDriveConfigEntry = ConfigEntry[GoogleDriveDataUpdateCoordinator]

_LOGGER = logging.getLogger(__name__)


@dataclass
class GoogleDriveCoordinatorData:
"""Class to hold coordinator data."""

storage_quota: StorageQuotaData
email_address: str


class GoogleDriveDataUpdateCoordinator(
DataUpdateCoordinator[GoogleDriveCoordinatorData]
):
"""Class to manage fetching Google Drive data from single endpoint."""

client: DriveClient
config_entry: GoogleDriveConfigEntry
_email_address: str
backup_folder_id: str

def __init__(
self,
hass: HomeAssistant,
*,
client: DriveClient,
backup_folder_id: str,
entry: GoogleDriveConfigEntry,
) -> None:
"""Initialize Google Drive data updater."""
self.client = client
self.backup_folder_id = backup_folder_id

super().__init__(
hass,
_LOGGER,
config_entry=entry,
name=DOMAIN,
update_interval=SCAN_INTERVAL,
)

async def _async_setup(self) -> None:
"""Do initialization logic."""
self._email_address = await self.client.async_get_email_address()

async def _async_update_data(self) -> GoogleDriveCoordinatorData:
"""Fetch data from Google Drive."""
try:
storage_quota = await self.client.async_get_storage_quota()
return GoogleDriveCoordinatorData(
storage_quota=storage_quota,
email_address=self._email_address,
)
Comment thread
mik-laj marked this conversation as resolved.
Outdated
except GoogleDriveApiError as error:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="invalid_response_google_drive_error",
translation_placeholders={"error": str(error)},
) from error
25 changes: 25 additions & 0 deletions homeassistant/components/google_drive/entity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""Define the Google Drive entity."""

from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from .const import DOMAIN, DRIVE_FOLDER_URL_PREFIX
from .coordinator import GoogleDriveDataUpdateCoordinator


class GoogleDriveEntity(CoordinatorEntity[GoogleDriveDataUpdateCoordinator]):
"""Defines a base Google Drive entity."""

_attr_has_entity_name = True

@property
def device_info(self) -> DeviceInfo:
"""Return device information about this Google Drive device."""
return DeviceInfo(
identifiers={(DOMAIN, str(self.coordinator.config_entry.unique_id))},
name=self.coordinator.data.email_address,
manufacturer="Google",
model="Google Drive",
configuration_url=f"{DRIVE_FOLDER_URL_PREFIX}{self.coordinator.backup_folder_id}",
entry_type=DeviceEntryType.SERVICE,
)
18 changes: 18 additions & 0 deletions homeassistant/components/google_drive/icons.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"entity": {
"sensor": {
"storage_total": {
"default": "mdi:database"
},
"storage_used": {
"default": "mdi:database"
},
"storage_used_in_drive": {
"default": "mdi:database"
},
"storage_used_in_drive_trash": {
"default": "mdi:database"
}
}
}
}
2 changes: 1 addition & 1 deletion homeassistant/components/google_drive/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["google_drive_api"],
"quality_scale": "platinum",
"quality_scale": "silver",
"requirements": ["python-google-drive-api==0.1.0"]
}
67 changes: 20 additions & 47 deletions homeassistant/components/google_drive/quality_scale.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ rules:
action-setup:
status: exempt
comment: No actions.
appropriate-polling:
status: exempt
comment: No polling.
appropriate-polling: done
brands: done
common-modules: done
config-flow-test-coverage: done
Expand All @@ -17,12 +15,8 @@ rules:
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup:
status: exempt
comment: No entities.
entity-unique-id:
status: exempt
comment: No entities.
entity-event-setup: done
entity-unique-id: done
has-entity-name:
status: exempt
comment: No entities.
Expand All @@ -38,39 +32,24 @@ rules:
status: exempt
comment: No configuration options.
docs-installation-parameters: done
entity-unavailable:
status: exempt
comment: No entities.
entity-unavailable: done
integration-owner: done
log-when-unavailable:
status: exempt
comment: No entities.
parallel-updates:
status: exempt
comment: No actions and no entities.
log-when-unavailable: done
parallel-updates: done
reauthentication-flow: done
test-coverage: done

# Gold
devices:
status: exempt
comment: No devices.
diagnostics:
status: exempt
comment: No data to diagnose.
devices: done
diagnostics: todo

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid downgrading the quality scale, can you add diagnostics? We could make an exception adding 2 platforms. Or you could add an empty diagnostics in a separate PR that we can merge first.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added diagnostics in this PR.

discovery-update-info:
status: exempt
comment: No discovery.
discovery:
status: exempt
comment: No discovery.
docs-data-update:
status: exempt
comment: No updates.
docs-examples:
status: exempt
comment: |
This integration only serves backup.
docs-data-update: done
docs-examples: done
docs-known-limitations: done
docs-supported-devices:
status: exempt
Expand All @@ -79,20 +58,13 @@ rules:
docs-troubleshooting: done
docs-use-cases: done
dynamic-devices:
status: exempt
comment: No devices.
entity-category:
status: exempt
comment: No entities.
entity-device-class:
status: exempt
comment: No entities.
entity-disabled-by-default:
status: exempt
comment: No entities.
entity-translations:
status: exempt
comment: No entities.
status: done
comment: |
This integration has a fixed single service.
entity-category: done
entity-device-class: done
entity-disabled-by-default: done
entity-translations: done
exception-translations: done
icon-translations:
status: exempt
Expand All @@ -104,8 +76,9 @@ rules:
status: exempt
comment: No repairs.
stale-devices:
status: exempt
comment: No devices.
status: done
comment: |
This integration has a fixed single service.

# Platinum
async-dependency: done
Expand Down
Loading
Loading