Skip to content

Commit

Permalink
BREAKING CHANGE: Reworked configuration of visible albums
Browse files Browse the repository at this point in the history
  • Loading branch information
Daanoz committed Feb 13, 2023
1 parent 73ea31e commit 1e9e6ce
Show file tree
Hide file tree
Showing 8 changed files with 179 additions and 46 deletions.
6 changes: 2 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
[![Discord][discord-shield]][discord]
[![Community Forum][forum-shield]][forum]

This integration adds for every album in your Google Photos account a `camera` entity to your Home Assistant setup. The entity will be showing media from your Google Photo album so you can add some personalization to your dashboards.
This integration allows you to add albums from your Google Photos account as a `camera` entity to your Home Assistant setup. The entity will be showing media from your Google Photo album so you can add some personalization to your dashboards.

**This component will set up the following platforms.**

Expand Down Expand Up @@ -64,7 +64,7 @@ Platform | Description
1. The page will now display *Link account to Home Assistant?*, note Your instance URL. If this is not correct, please refer to [My Home Assistant](https://next.home-assistant.io/integrations/my). If everything looks good, click **Link Account**.
1. You may close the window, and return back to Home Assistant where you should see a Success! message from Home Assistant.

After the setup is complete a device will be created with multiple entities, each entity being one of the albums in the linked account. Note that only one album will enabled initially to avoid overloading your setup, feel free to enable the ones you want to use.
After the setup is complete a device will be created with entity for your favorite photos. To add more albums from you account, click configure on the integration card.

## Card setups

Expand Down Expand Up @@ -109,11 +109,9 @@ data:
## Notes / Remarks / Limitations

- Currently the album media is cached for 30 minutes.
- New albums can only be added by reloading the integration or restarting HA.

## Future plans

- Improved updating of albums
- Support for videos
- Support loading media using [content categories](https://developers.google.com/photos/library/guides/apply-filters#content-categories)
- Support loading media filtered by date/time
Expand Down
21 changes: 21 additions & 0 deletions custom_components/google_photos/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Support for Google Photos."""
from __future__ import annotations

import logging
from aiohttp.client_exceptions import ClientError, ClientResponseError

from homeassistant.config_entries import ConfigEntry, ConfigEntryState
Expand All @@ -20,8 +21,23 @@
PLATFORMS = [Platform.CAMERA]


async def async_migrate_entry(hass, config_entry: ConfigEntry):
"""Migrate old entry."""
_LOGGER = logging.getLogger(__name__)
_LOGGER.debug("Migrating from version %s", config_entry.version)

if config_entry.version == 1:
_LOGGER.error("Migration failed, please remove / add integration.")
return False

_LOGGER.info("Migration to version %s successful", config_entry.version)

return True


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Google Photos from a config entry."""
entry.async_on_unload(entry.add_update_listener(update_listener))
implementation = await async_get_config_entry_implementation(hass, entry)
session = OAuth2Session(hass, entry, implementation)
auth = AsyncConfigEntryAuth(async_get_clientsession(hass), session)
Expand All @@ -42,6 +58,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return True


async def update_listener(hass, entry):
"""Handle options update."""
await hass.config_entries.async_reload(entry.entry_id)


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
Expand Down
76 changes: 39 additions & 37 deletions custom_components/google_photos/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
MODE_OPTIONS,
CONF_WRITEMETADATA,
WRITEMETADATA_DEFAULT_OPTION,
CONF_ALBUM_ID,
CONF_ALBUM_ID_FAVORITES,
)

SERVICE_NEXT_MEDIA = "next_media"
Expand All @@ -61,32 +63,27 @@ async def async_setup_entry(
"""Set up the Google Photos camera."""
auth: AsyncConfigEntryAuth = hass.data[DOMAIN][entry.entry_id]
service = await auth.get_resource(hass)
album_ids = entry.options[CONF_ALBUM_ID]

def _get_albums() -> List[Album]:
result = service.albums().list(pageSize=50).execute()
album_list = result["albums"]
while "nextPageToken" in result and result["nextPageToken"] != "":
result = (
service.albums()
.list(pageSize=50, pageToken=result["nextPageToken"])
.execute()
)
album_list = album_list + result["albums"]
album_list = []
for album_id in album_ids:
if album_id == CONF_ALBUM_ID_FAVORITES:
album_list.append(
GooglePhotosFavoritesCamera(
hass.data[DOMAIN][entry.entry_id], CAMERA_TYPE
)
)
else:
album = service.albums().get(albumId=album_id).execute()
album_list.append(
GooglePhotosAlbumCamera(
hass.data[DOMAIN][entry.entry_id], album, CAMERA_TYPE
)
)
return album_list

albums = await hass.async_add_executor_job(_get_albums)

def as_camera(album: Album):
return GooglePhotosAlbumCamera(
hass.data[DOMAIN][entry.entry_id], album, CAMERA_TYPE
)

entities = [
GooglePhotosFavoritesCamera(hass.data[DOMAIN][entry.entry_id], CAMERA_TYPE)
] + list(map(as_camera, albums))
if len(entities) > 0:
entities[0].enabled_by_default()

entities = await hass.async_add_executor_job(_get_albums)
platform = entity_platform.async_get_current_platform()
platform.async_register_entity_service(
SERVICE_NEXT_MEDIA,
Expand All @@ -105,7 +102,6 @@ class GooglePhotosBaseCamera(Camera):

_auth: AsyncConfigEntryAuth
_attr_has_entity_name = True
_attr_entity_registry_enabled_default = False
_attr_icon = "mdi:image"

_media_id: str | None = None
Expand All @@ -118,9 +114,7 @@ class GooglePhotosBaseCamera(Camera):
_album_timestamp = None

def __init__(
self,
auth: AsyncConfigEntryAuth,
description: EntityDescription,
self, auth: AsyncConfigEntryAuth, description: EntityDescription
) -> None:
"""Initialize a Google Photos Base Camera class."""
super().__init__()
Expand All @@ -131,18 +125,9 @@ def __init__(
self._attr_is_on = True
self._attr_is_recording = False
self._attr_is_streaming = False
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, auth.oauth_session.config_entry.entry_id)},
manufacturer=MANUFACTURER,
name="Google Photos Library",
)
self._attr_should_poll = False
self._attr_extra_state_attributes = {}

def enabled_by_default(self) -> None:
"""Set camera as enabled by default."""
self._attr_entity_registry_enabled_default = True

async def next_media(self, mode=None):
"""Load the next media."""
if self._is_loading_next:
Expand Down Expand Up @@ -297,8 +282,6 @@ def _get_album_media_list(
raise NotImplementedError("To be implemented by subclass")




class GooglePhotosAlbumCamera(GooglePhotosBaseCamera):
"""Representation of a Google Photos Album camera."""

Expand All @@ -316,6 +299,14 @@ def __init__(
self._attr_name = album["title"]
self._attr_unique_id = album["id"]
self._set_media_id(album["coverPhotoMediaItemId"])
self._attr_device_info = DeviceInfo(
identifiers={
(DOMAIN, auth.oauth_session.config_entry.entry_id, album["id"])
},
manufacturer=MANUFACTURER,
name="Google Photos - " + album["title"],
configuration_url=album["productUrl"],
)

def _get_album_media_list(
self, service: PhotosLibraryService
Expand Down Expand Up @@ -360,6 +351,17 @@ def __init__(
super().__init__(auth, description)
self._attr_name = "Favorites"
self._attr_unique_id = "library_favorites"
self._attr_device_info = DeviceInfo(
identifiers={
(
DOMAIN,
auth.oauth_session.config_entry.entry_id,
CONF_ALBUM_ID_FAVORITES,
)
},
manufacturer=MANUFACTURER,
name="Google Photos - Favorites",
)

async def async_added_to_hass(self) -> None:
await self.next_media()
Expand Down
91 changes: 86 additions & 5 deletions custom_components/google_photos/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from collections.abc import Mapping
import logging
from typing import Any
from typing import Any, List
import voluptuous as vol

from google.oauth2.credentials import Credentials
Expand All @@ -16,6 +16,7 @@
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import config_entry_oauth2_flow

from .api_types import PhotosLibraryService, Album
from .const import (
DEFAULT_ACCESS,
DOMAIN,
Expand All @@ -26,7 +27,9 @@
INTERVAL_OPTIONS,
INTERVAL_DEFAULT_OPTION,
CONF_WRITEMETADATA,
WRITEMETADATA_DEFAULT_OPTION
WRITEMETADATA_DEFAULT_OPTION,
CONF_ALBUM_ID,
CONF_ALBUM_ID_FAVORITES,
)


Expand All @@ -36,6 +39,7 @@ class OAuth2FlowHandler(
"""Config flow to handle Google Photos OAuth2 authentication."""

DOMAIN = DOMAIN
VERSION = 2

reauth_entry: ConfigEntry | None = None

Expand Down Expand Up @@ -89,7 +93,9 @@ def _get_profile() -> dict[str, Any]:
await self.async_set_unique_id(email)
self._abort_if_unique_id_configured()

return self.async_create_entry(title=email, data=data)
options = dict()
options[CONF_ALBUM_ID] = [CONF_ALBUM_ID_FAVORITES]
return self.async_create_entry(title=email, data=data, options=options)

@staticmethod
@callback
Expand All @@ -107,10 +113,85 @@ def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
"""Initialize options flow."""
self.config_entry = config_entry

@property
def logger(self) -> logging.Logger:
"""Return logger."""
return logging.getLogger(__name__)

async def _get_albumselect_schema(self) -> vol.Schema:
"""Return album selection form"""

credentials = Credentials(self.config_entry.data[CONF_TOKEN][CONF_ACCESS_TOKEN])

def get_photoslibrary() -> PhotosLibraryService:
return build(
"photoslibrary",
"v1",
credentials=credentials,
static_discovery=False,
)

def get_albums() -> List[Album]:
service: PhotosLibraryService = get_photoslibrary()
result = service.albums().list(pageSize=50).execute()
album_list = result["albums"]
while "nextPageToken" in result and result["nextPageToken"] != "":
result = (
service.albums()
.list(pageSize=50, pageToken=result["nextPageToken"])
.execute()
)
album_list = album_list + result["albums"]
return album_list

albums = await self.hass.async_add_executor_job(get_albums)
album_selection = dict({CONF_ALBUM_ID_FAVORITES: "Favorites"})
for album in albums:
album_selection[album["id"]] = album["title"]

return vol.Schema(
{
vol.Required(CONF_ALBUM_ID): vol.In(album_selection),
}
)

async def async_step_init(self, user_input=None):
"""Handle options flow."""
return self.async_show_menu(
step_id="init",
menu_options=["albumselect", "settings"],
description_placeholders={
"model": "Example model",
},
)

async def async_step_albumselect(
self, user_input: dict[str, Any] = None
) -> FlowResult:
"""Set the album used."""
self.logger.debug(
"async_albumselect_confirm called with user_input: %s", user_input
)

# user input was not provided.
if user_input is None:
data_schema = await self._get_albumselect_schema()
return self.async_show_form(step_id="albumselect", data_schema=data_schema)

album_id = user_input[CONF_ALBUM_ID]
albums = self.config_entry.options.get(CONF_ALBUM_ID, []).copy()
if album_id not in albums:
albums.append(album_id)
data = self.config_entry.options.copy()
data.update({CONF_ALBUM_ID: albums})
return self.async_create_entry(title="", data=data)

async def async_step_settings(self, user_input=None):
"""Handle options flow."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
data = self.config_entry.options.copy()
data.update(user_input)
return self.async_create_entry(title="", data=data)

data_schema = vol.Schema(
{
Expand All @@ -134,4 +215,4 @@ async def async_step_init(self, user_input=None):
): bool,
}
)
return self.async_show_form(step_id="init", data_schema=data_schema)
return self.async_show_form(step_id="settings", data_schema=data_schema)
3 changes: 3 additions & 0 deletions custom_components/google_photos/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
"https://www.googleapis.com/auth/photoslibrary.readonly",
]

CONF_ALBUM_ID = "album_id"
CONF_ALBUM_ID_FAVORITES = "FAVORITES"

CONF_MODE = "mode"
MODE_OPTION_RANDOM = "RANDOM"
MODE_OPTION_ALBUM_ORDER = "ALBUM_ORDER"
Expand Down
14 changes: 14 additions & 0 deletions custom_components/google_photos/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,20 @@
"options": {
"step": {
"init": {
"menu_options": {
"albumselect": "Select album",
"settings": "Settings"
},
"title": "Adjust Google Photos options"
},
"albumselect": {
"data": {
"album_id": "Album"
},
"title": "Select album to add",
"description": "Album will be added as a seperate entity after a short period of time."
},
"settings": {
"data": {
"mode": "Selection mode.",
"interval": "Refresh interval",
Expand Down
Loading

0 comments on commit 1e9e6ce

Please sign in to comment.