Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .strict-typing
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ homeassistant.components.actiontec.*
homeassistant.components.adax.*
homeassistant.components.adguard.*
homeassistant.components.aftership.*
homeassistant.components.ai_task.*
homeassistant.components.air_quality.*
homeassistant.components.airgradient.*
homeassistant.components.airly.*
Expand Down Expand Up @@ -209,6 +210,7 @@ homeassistant.components.firefly_iii.*
homeassistant.components.fitbit.*
homeassistant.components.flexit_bacnet.*
homeassistant.components.flux_led.*
homeassistant.components.folder_watcher.*
homeassistant.components.forecast_solar.*
homeassistant.components.fritz.*
homeassistant.components.fritzbox.*
Expand Down Expand Up @@ -298,6 +300,7 @@ homeassistant.components.iotty.*
homeassistant.components.ipp.*
homeassistant.components.iqvia.*
homeassistant.components.iron_os.*
homeassistant.components.isal.*
homeassistant.components.islamic_prayer_times.*
homeassistant.components.isy994.*
homeassistant.components.jellyfin.*
Expand All @@ -308,6 +311,7 @@ homeassistant.components.knocki.*
homeassistant.components.knx.*
homeassistant.components.kraken.*
homeassistant.components.kulersky.*
homeassistant.components.labs.*
homeassistant.components.lacrosse.*
homeassistant.components.lacrosse_view.*
homeassistant.components.lamarzocco.*
Expand Down Expand Up @@ -403,6 +407,7 @@ homeassistant.components.opnsense.*
homeassistant.components.opower.*
homeassistant.components.oralb.*
homeassistant.components.otbr.*
homeassistant.components.otp.*
homeassistant.components.overkiz.*
homeassistant.components.overseerr.*
homeassistant.components.p1_monitor.*
Expand Down Expand Up @@ -438,10 +443,12 @@ homeassistant.components.radarr.*
homeassistant.components.radio_browser.*
homeassistant.components.rainforest_raven.*
homeassistant.components.rainmachine.*
homeassistant.components.random.*
homeassistant.components.raspberry_pi.*
homeassistant.components.rdw.*
homeassistant.components.recollect_waste.*
homeassistant.components.recorder.*
homeassistant.components.recovery_mode.*
homeassistant.components.redgtech.*
homeassistant.components.remember_the_milk.*
homeassistant.components.remote.*
Expand Down Expand Up @@ -473,6 +480,7 @@ homeassistant.components.schlage.*
homeassistant.components.scrape.*
homeassistant.components.script.*
homeassistant.components.search.*
homeassistant.components.season.*
homeassistant.components.select.*
homeassistant.components.sensibo.*
homeassistant.components.sensirion_ble.*
Expand Down Expand Up @@ -566,6 +574,7 @@ homeassistant.components.update.*
homeassistant.components.uptime.*
homeassistant.components.uptime_kuma.*
homeassistant.components.uptimerobot.*
homeassistant.components.usage_prediction.*
homeassistant.components.usb.*
homeassistant.components.uvc.*
homeassistant.components.vacuum.*
Expand All @@ -584,6 +593,7 @@ homeassistant.components.water_heater.*
homeassistant.components.watts.*
homeassistant.components.watttime.*
homeassistant.components.weather.*
homeassistant.components.web_rtc.*
homeassistant.components.webhook.*
homeassistant.components.webostv.*
homeassistant.components.websocket_api.*
Expand Down
24 changes: 17 additions & 7 deletions homeassistant/components/aws_s3/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@
import logging
from typing import cast

from aiobotocore.client import AioBaseClient as S3Client
from aiobotocore.session import AioSession
from botocore.exceptions import ClientError, ConnectionError, ParamValidationError

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady

Expand All @@ -21,9 +20,9 @@
DATA_BACKUP_AGENT_LISTENERS,
DOMAIN,
)
from .coordinator import S3ConfigEntry, S3DataUpdateCoordinator

type S3ConfigEntry = ConfigEntry[S3Client]

_PLATFORMS = (Platform.SENSOR,)

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -64,19 +63,30 @@ async def async_setup_entry(hass: HomeAssistant, entry: S3ConfigEntry) -> bool:
translation_key="cannot_connect",
) from err

entry.runtime_data = client
coordinator = S3DataUpdateCoordinator(
hass,
entry=entry,
client=client,
)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator

def notify_backup_listeners() -> None:
for listener in hass.data.get(DATA_BACKUP_AGENT_LISTENERS, []):
listener()

entry.async_on_unload(entry.async_on_state_change(notify_backup_listeners))

await hass.config_entries.async_forward_entry_setups(entry, _PLATFORMS)

return True


async def async_unload_entry(hass: HomeAssistant, entry: S3ConfigEntry) -> bool:
"""Unload a config entry."""
client = entry.runtime_data
await client.__aexit__(None, None, None)
unload_ok = await hass.config_entries.async_unload_platforms(entry, _PLATFORMS)
if not unload_ok:
return False
coordinator = entry.runtime_data
await coordinator.client.__aexit__(None, None, None)
return True
34 changes: 4 additions & 30 deletions homeassistant/components/aws_s3/backup.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

from . import S3ConfigEntry
from .const import CONF_BUCKET, DATA_BACKUP_AGENT_LISTENERS, DOMAIN
from .helpers import async_list_backups_from_s3

_LOGGER = logging.getLogger(__name__)
CACHE_TTL = 300
Expand Down Expand Up @@ -93,7 +94,7 @@ class S3BackupAgent(BackupAgent):
def __init__(self, hass: HomeAssistant, entry: S3ConfigEntry) -> None:
"""Initialize the S3 agent."""
super().__init__()
self._client = entry.runtime_data
self._client = entry.runtime_data.client
self._bucket: str = entry.data[CONF_BUCKET]
self.name = entry.title
self.unique_id = entry.entry_id
Expand Down Expand Up @@ -316,35 +317,8 @@ async def _list_backups(self) -> dict[str, AgentBackup]:
if time() <= self._cache_expiration:
return self._backup_cache

backups = {}
paginator = self._client.get_paginator("list_objects_v2")
metadata_files: list[dict[str, Any]] = []
async for page in paginator.paginate(Bucket=self._bucket):
metadata_files.extend(
obj
for obj in page.get("Contents", [])
if obj["Key"].endswith(".metadata.json")
)

for metadata_file in metadata_files:
try:
# Download and parse metadata file
metadata_response = await self._client.get_object(
Bucket=self._bucket, Key=metadata_file["Key"]
)
metadata_content = await metadata_response["Body"].read()
metadata_json = json.loads(metadata_content)
except (BotoCoreError, json.JSONDecodeError) as err:
_LOGGER.warning(
"Failed to process metadata file %s: %s",
metadata_file["Key"],
err,
)
continue
backup = AgentBackup.from_dict(metadata_json)
backups[backup.backup_id] = backup

self._backup_cache = backups
backups_list = await async_list_backups_from_s3(self._client, self._bucket)
self._backup_cache = {b.backup_id: b for b in backups_list}
self._cache_expiration = time() + CACHE_TTL

return self._backup_cache
70 changes: 70 additions & 0 deletions homeassistant/components/aws_s3/coordinator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""DataUpdateCoordinator for AWS S3."""

from __future__ import annotations

from dataclasses import dataclass
from datetime import timedelta
import logging

from aiobotocore.client import AioBaseClient as S3Client
from botocore.exceptions import BotoCoreError

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

from .const import CONF_BUCKET, DOMAIN
from .helpers import async_list_backups_from_s3

SCAN_INTERVAL = timedelta(hours=6)

type S3ConfigEntry = ConfigEntry[S3DataUpdateCoordinator]

_LOGGER = logging.getLogger(__name__)


@dataclass
class SensorData:
"""Class to represent sensor data."""

all_backups_size: int


class S3DataUpdateCoordinator(DataUpdateCoordinator[SensorData]):
"""Class to manage fetching AWS S3 data from single endpoint."""

config_entry: S3ConfigEntry
client: S3Client

def __init__(
self,
hass: HomeAssistant,
*,
entry: S3ConfigEntry,
client: S3Client,
) -> None:
"""Initialize AWS S3 data updater."""
super().__init__(
hass,
_LOGGER,
config_entry=entry,
name=DOMAIN,
update_interval=SCAN_INTERVAL,
)
self.client = client
self._bucket: str = entry.data[CONF_BUCKET]

async def _async_update_data(self) -> SensorData:
"""Fetch data from AWS S3."""
try:
backups = await async_list_backups_from_s3(self.client, self._bucket)
except BotoCoreError as error:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="error_fetching_data",
) from error

all_backups_size = sum(b.size for b in backups)
return SensorData(
all_backups_size=all_backups_size,
)
33 changes: 33 additions & 0 deletions homeassistant/components/aws_s3/entity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""Define the AWS S3 entity."""

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

from .const import CONF_BUCKET, DOMAIN
from .coordinator import S3DataUpdateCoordinator


class S3Entity(CoordinatorEntity[S3DataUpdateCoordinator]):
"""Defines a base AWS S3 entity."""

_attr_has_entity_name = True

def __init__(
self, coordinator: S3DataUpdateCoordinator, description: EntityDescription
) -> None:
"""Initialize an AWS S3 entity."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{description.key}"

@property
def device_info(self) -> DeviceInfo:
"""Return device information about this AWS S3 device."""
return DeviceInfo(
identifiers={(DOMAIN, self.coordinator.config_entry.entry_id)},
name=f"Bucket {self.coordinator.config_entry.data[CONF_BUCKET]}",
manufacturer="AWS",
model="AWS S3",
entry_type=DeviceEntryType.SERVICE,
)
57 changes: 57 additions & 0 deletions homeassistant/components/aws_s3/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""Helpers for the AWS S3 integration."""

from __future__ import annotations

import json
import logging
from typing import Any

from aiobotocore.client import AioBaseClient as S3Client
from botocore.exceptions import BotoCoreError

from homeassistant.components.backup import AgentBackup

_LOGGER = logging.getLogger(__name__)


async def async_list_backups_from_s3(
client: S3Client,
bucket: str,
) -> list[AgentBackup]:
"""List backups from an S3 bucket by reading metadata files."""
paginator = client.get_paginator("list_objects_v2")
metadata_files: list[dict[str, Any]] = []
async for page in paginator.paginate(Bucket=bucket):
metadata_files.extend(
obj
for obj in page.get("Contents", [])
if obj["Key"].endswith(".metadata.json")
)

backups: list[AgentBackup] = []
for metadata_file in metadata_files:
try:
metadata_response = await client.get_object(
Bucket=bucket, Key=metadata_file["Key"]
)
metadata_content = await metadata_response["Body"].read()
metadata_json = json.loads(metadata_content)
except (BotoCoreError, json.JSONDecodeError) as err:
_LOGGER.warning(
"Failed to process metadata file %s: %s",
metadata_file["Key"],
err,
)
continue
try:
backup = AgentBackup.from_dict(metadata_json)
except (KeyError, TypeError, ValueError) as err:
_LOGGER.warning(
"Failed to parse metadata in file %s: %s",
metadata_file["Key"],
err,
)
continue
backups.append(backup)

return backups
3 changes: 2 additions & 1 deletion homeassistant/components/aws_s3/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
"name": "AWS S3",
"codeowners": ["@tomasbedrich"],
"config_flow": true,
"dependencies": ["backup"],
"documentation": "https://www.home-assistant.io/integrations/aws_s3",
"integration_type": "service",
"iot_class": "cloud_push",
"iot_class": "cloud_polling",
"loggers": ["aiobotocore"],
"quality_scale": "bronze",
"requirements": ["aiobotocore==2.21.1"]
Expand Down
Loading
Loading