Skip to content

Commit bba3e89

Browse files
committed
Add support for albums, fix refresh interval
1 parent 5bf1206 commit bba3e89

File tree

8 files changed

+268
-60
lines changed

8 files changed

+268
-60
lines changed

custom_components/immich/__init__.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,13 @@
22
from __future__ import annotations
33

44
from homeassistant.config_entries import ConfigEntry
5-
from homeassistant.const import Platform
6-
from homeassistant.const import CONF_HOST, CONF_API_KEY
5+
from homeassistant.const import CONF_API_KEY, CONF_HOST, Platform
76
from homeassistant.core import HomeAssistant
8-
from datetime import timedelta
97

108
from .const import DOMAIN
119
from .hub import ImmichHub, InvalidAuth
1210

1311
PLATFORMS: list[Platform] = [Platform.IMAGE]
14-
SCAN_INTERVAL = timedelta(minutes=5)
1512

1613

1714
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:

custom_components/immich/config_flow.py

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,19 @@
33

44
import logging
55
from typing import Any
6-
from url_normalize import url_normalize
76
from urllib.parse import urlparse
87

8+
from url_normalize import url_normalize
99
import voluptuous as vol
1010

1111
from homeassistant import config_entries
12-
from homeassistant.const import CONF_HOST, CONF_API_KEY
13-
from homeassistant.core import HomeAssistant
12+
from homeassistant.const import CONF_API_KEY, CONF_HOST
13+
from homeassistant.core import HomeAssistant, callback
1414
from homeassistant.data_entry_flow import FlowResult
15+
from homeassistant.helpers import config_validation as cv
1516

16-
from .const import DOMAIN
17-
from .hub import ImmichHub, InvalidAuth, CannotConnect
17+
from .const import CONF_WATCHED_ALBUMS, DOMAIN
18+
from .hub import CannotConnect, ImmichHub, InvalidAuth
1819

1920
_LOGGER = logging.getLogger(__name__)
2021

@@ -40,9 +41,13 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
4041
if not await hub.authenticate():
4142
raise InvalidAuth
4243

44+
user_info = await hub.get_my_user_info()
45+
username = user_info["name"]
46+
clean_hostname = urlparse(url).hostname
47+
4348
# Return info that you want to store in the config entry.
4449
return {
45-
"title": urlparse(url).hostname,
50+
"title": f"{username} @ {clean_hostname}",
4651
"data": {CONF_HOST: url, CONF_API_KEY: api_key},
4752
}
4853

@@ -73,3 +78,58 @@ async def async_step_user(
7378
return self.async_show_form(
7479
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
7580
)
81+
82+
@staticmethod
83+
@callback
84+
def async_get_options_flow(
85+
config_entry: config_entries.ConfigEntry,
86+
) -> config_entries.OptionsFlow:
87+
"""Create the options flow."""
88+
return OptionsFlowHandler(config_entry)
89+
90+
91+
class OptionsFlowHandler(config_entries.OptionsFlow):
92+
"""Immich options flow handler."""
93+
94+
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
95+
"""Initialize options flow."""
96+
self.config_entry = config_entry
97+
98+
async def async_step_init(
99+
self, user_input: dict[str, Any] | None = None
100+
) -> FlowResult:
101+
"""Manage the options."""
102+
if user_input is not None:
103+
return self.async_create_entry(title="", data=user_input)
104+
105+
# Get a connection to the hub in order to list the available albums
106+
url = url_normalize(self.config_entry.data[CONF_HOST])
107+
api_key = self.config_entry.data[CONF_API_KEY]
108+
hub = ImmichHub(host=url, api_key=api_key)
109+
110+
if not await hub.authenticate():
111+
raise InvalidAuth
112+
113+
# Get the list of albums and create a mapping of album id to album name
114+
albums = await hub.list_all_albums()
115+
album_map = {album["id"]: album["albumName"] for album in albums}
116+
117+
# Filter out any album ids that are no longer returned by the API
118+
current_albums_value = [
119+
album
120+
for album in self.config_entry.options.get(CONF_WATCHED_ALBUMS, [])
121+
if album in album_map
122+
]
123+
124+
# Allow the user to select which albums they want to create entities for
125+
return self.async_show_form(
126+
step_id="init",
127+
data_schema=vol.Schema(
128+
{
129+
vol.Required(
130+
CONF_WATCHED_ALBUMS,
131+
default=current_albums_value,
132+
): cv.multi_select(album_map)
133+
}
134+
),
135+
)

custom_components/immich/const.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
"""Constants for the immich integration."""
22

33
DOMAIN = "immich"
4+
CONF_WATCHED_ALBUMS = "watched_albums"

custom_components/immich/hub.py

Lines changed: 73 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
"""Hub for Immich integration."""
22
from __future__ import annotations
33

4-
import aiohttp
54
import logging
65
from urllib.parse import urljoin
7-
import random
6+
7+
import aiohttp
88

99
from homeassistant.exceptions import HomeAssistantError
1010

@@ -33,9 +33,9 @@ async def authenticate(self) -> bool:
3333
_LOGGER.error("Error from API: body=%s", raw_result)
3434
return False
3535

36-
json_result = await response.json()
36+
auth_result = await response.json()
3737

38-
if not json_result.get("authStatus"):
38+
if not auth_result.get("authStatus"):
3939
raw_result = await response.text()
4040
_LOGGER.error("Error from API: body=%s", raw_result)
4141
return False
@@ -45,21 +45,25 @@ async def authenticate(self) -> bool:
4545
_LOGGER.error("Error connecting to the API: %s", exception)
4646
raise CannotConnect from exception
4747

48-
async def get_random_picture(self) -> dict | None:
49-
"""Get a random picture from the API."""
50-
assets = [
51-
asset for asset in await self._list_favorites() if asset["type"] == "IMAGE"
52-
]
48+
async def get_my_user_info(self) -> dict:
49+
"""Get user info."""
50+
try:
51+
async with aiohttp.ClientSession() as session:
52+
url = urljoin(self.host, "/api/user/me")
53+
headers = {"Accept": "application/json", _HEADER_API_KEY: self.api_key}
5354

54-
if not assets:
55-
_LOGGER.error("No assets found in favorites")
56-
return None
55+
async with session.get(url=url, headers=headers) as response:
56+
if response.status != 200:
57+
raw_result = await response.text()
58+
_LOGGER.error("Error from API: body=%s", raw_result)
59+
raise ApiError()
5760

58-
# Select random item in list
59-
random_asset = random.choice(assets)
61+
user_info: dict = await response.json()
6062

61-
_LOGGER.debug("Random asset: %s", random_asset)
62-
return random_asset
63+
return user_info
64+
except aiohttp.ClientError as exception:
65+
_LOGGER.error("Error connecting to the API: %s", exception)
66+
raise CannotConnect from exception
6367

6468
async def download_asset(self, asset_id: str) -> bytes:
6569
"""Download the asset."""
@@ -78,7 +82,8 @@ async def download_asset(self, asset_id: str) -> bytes:
7882
_LOGGER.error("Error connecting to the API: %s", exception)
7983
raise CannotConnect from exception
8084

81-
async def _list_favorites(self) -> list[dict]:
85+
async def list_favorite_images(self) -> list[dict]:
86+
"""List all favorite images."""
8287
try:
8388
async with aiohttp.ClientSession() as session:
8489
url = urljoin(self.host, "/api/asset?isFavorite=true")
@@ -90,9 +95,58 @@ async def _list_favorites(self) -> list[dict]:
9095
_LOGGER.error("Error from API: body=%s", raw_result)
9196
raise ApiError()
9297

93-
json_result = await response.json()
98+
assets: list[dict] = await response.json()
99+
100+
filtered_assets: list[dict] = [
101+
asset for asset in assets if asset["type"] == "IMAGE"
102+
]
103+
104+
return filtered_assets
105+
except aiohttp.ClientError as exception:
106+
_LOGGER.error("Error connecting to the API: %s", exception)
107+
raise CannotConnect from exception
108+
109+
async def list_all_albums(self) -> list[dict]:
110+
"""List all albums."""
111+
try:
112+
async with aiohttp.ClientSession() as session:
113+
url = urljoin(self.host, "/api/album")
114+
headers = {"Accept": "application/json", _HEADER_API_KEY: self.api_key}
115+
116+
async with session.get(url=url, headers=headers) as response:
117+
if response.status != 200:
118+
raw_result = await response.text()
119+
_LOGGER.error("Error from API: body=%s", raw_result)
120+
raise ApiError()
121+
122+
album_list: list[dict] = await response.json()
123+
124+
return album_list
125+
except aiohttp.ClientError as exception:
126+
_LOGGER.error("Error connecting to the API: %s", exception)
127+
raise CannotConnect from exception
128+
129+
async def list_album_images(self, album_id: str) -> list[dict]:
130+
"""List all images in an album."""
131+
try:
132+
async with aiohttp.ClientSession() as session:
133+
url = urljoin(self.host, f"/api/album/{album_id}")
134+
headers = {"Accept": "application/json", _HEADER_API_KEY: self.api_key}
135+
136+
async with session.get(url=url, headers=headers) as response:
137+
if response.status != 200:
138+
raw_result = await response.text()
139+
_LOGGER.error("Error from API: body=%s", raw_result)
140+
raise ApiError()
141+
142+
album_info: dict = await response.json()
143+
assets: list[dict] = album_info["assets"]
144+
145+
filtered_assets: list[dict] = [
146+
asset for asset in assets if asset["type"] == "IMAGE"
147+
]
94148

95-
return json_result
149+
return filtered_assets
96150
except aiohttp.ClientError as exception:
97151
_LOGGER.error("Error connecting to the API: %s", exception)
98152
raise CannotConnect from exception

0 commit comments

Comments
 (0)