Building a Family Photo Display with Immich and Home Assistant

Every family takes thousands of photos. They sit on phones, accumulate in cloud accounts, and rarely get looked at again. We wanted to change that. The goal was simple: build a photo display that lives in our common area, automatically shows curated family photos in a beautiful slideshow, and wakes up when someone walks by. No subscriptions, no cloud dependency, and no fiddling with SD cards or USB drives.

What started as a weekend project turned into a surprisingly satisfying pipeline: iPhones upload photos to a self-hosted Immich instance, a custom Python server pulls from a shared album and serves them to a Home Assistant dashboard, and a Samsung tablet on the wall brings it all to life with motion-activated wake. Here is how we built it.

Why Not Just Use Immich's Web UI?

Immich is excellent photo management software. Its web interface works well for browsing and organizing photos. But it was never designed to be a passive display. Opening an Immich album in a browser gives you a grid of thumbnails, not a fullscreen slideshow. You could click into a photo and manually advance, but that defeats the purpose of a hands-free display.

We also needed specific behavior that no existing tool provided out of the box: automatic rotation through a curated album, a shuffle algorithm that guarantees every photo gets shown before any repeats, smooth crossfade transitions, and an HTTP API that Home Assistant could talk to. Commercial digital photo frame services solve some of this, but they come with subscriptions, cloud dependencies, and zero flexibility. We wanted something that runs entirely on our local network and does exactly what we need.

The Custom Photo Server

The heart of the system is a single-file Python HTTP server that bridges Immich and the display. We made a deliberate choice here: zero external dependencies. No Flask, no FastAPI, no pip install anything. Just Python's built-in http.server and urllib modules. On a home server that runs 24/7, fewer dependencies means fewer things that break on OS upgrades or Python version changes.

The server polls a shared Immich album called "Dashboard Display" every five minutes using the Immich API. Any family member can add or remove photos from this album through the Immich mobile app, and the display updates automatically on the next poll cycle. This is the curation layer: rather than dumping every photo from every camera roll, we intentionally choose which moments end up on the wall.

The server exposes four endpoints:

  • /display — Serves the slideshow HTML page with fullscreen layout, crossfade transitions, and Ken Burns zoom
  • /photo — Returns the current photo as a JPEG
  • /next — Advances to the next photo and returns it
  • /health — JSON status endpoint for monitoring

The Shuffle-Deck Algorithm

Random photo selection has an annoying property: with a small-to-medium album, you frequently see the same photo twice before you have seen every photo once. It is the birthday paradox applied to slideshows. Our solution is what we call a shuffle-deck algorithm, inspired by how a deck of cards works.

When the server starts (or when the album contents change), it builds a list of all photo IDs, shuffles them into a random order, and works through the list sequentially. Once every photo has been displayed, it reshuffles the entire deck and starts again. This guarantees that you see every photo in the album exactly once per cycle before any photo repeats. With a 200-photo album rotating every 10 seconds, that is over 30 minutes of unique content per cycle.

The Display Frontend

The /display endpoint serves a self-contained HTML page that runs the slideshow. The approach uses two stacked <img> elements for seamless crossfade transitions. While one image is visible, the next image loads behind it. Every 10 seconds, the front image fades out over 2.5 seconds while the back image fades in, then the roles swap.

A subtle Ken Burns effect adds life to the display. Each photo gets a slow, gentle zoom over its display duration, which prevents the slideshow from looking like a static picture frame. The effect is restrained enough that you barely notice it consciously, but the display feels more alive than a flat image swap.

/* Dual-image crossfade with Ken Burns zoom */
.slide {
  position: absolute;
  inset: 0;
  object-fit: contain;
  transition: opacity 2.5s ease-in-out;
  animation: kenburns 10s ease-in-out infinite;
}

@keyframes kenburns {
  0%   { transform: scale(1.0); }
  100% { transform: scale(1.05); }
}

The JavaScript is minimal: fetch /next every 10 seconds, swap which image is in front, and preload the next photo. No framework, no build step. The entire display page is served inline from the Python server.

Immich: The Photo Backend

Immich runs as a standard Docker Compose stack on the same server: the main server container, a machine learning container for facial recognition and smart search, Redis for caching, and PostgreSQL for metadata storage. We deliberately do not use Immich for automatic camera backup. Instead, family members manually upload selected photos through the Immich mobile app and add their favorites to the shared "Dashboard Display" album.

This manual curation step is important. Automatic backup means the display would show screenshots, receipts, work documents, and every failed attempt at a group photo. By making album membership intentional, every photo on the display is something someone in the family chose to share.

The Display Hardware

The primary display is a Samsung Galaxy Tab A9+ running Fully Kiosk Browser PLUS. Fully Kiosk is the unsung hero of this setup. It locks the tablet into a single-purpose kiosk mode, disabling the Android system UI, notifications, and navigation gestures. The tablet becomes a dedicated photo frame.

The standout feature is motion-activated wake using the tablet's front camera. Fully Kiosk's motion detection watches for movement through the camera feed at sensitivity 98 (nearly maximum). When someone walks into the room, the screen turns on and the slideshow is already running. After three minutes with no detected motion, the screen turns off to save power and extend the display's lifespan. No PIR sensors, no external hardware, no smart home motion detectors needed. The tablet's own camera handles it.

We also run the slideshow on an LG TV through its built-in web browser. The TV points to the same /display endpoint. Since the photo server manages rotation state centrally, both displays show the same photo simultaneously, which looks intentional when they are in the same room.

Home Assistant Integration

Home Assistant ties everything together with dashboards and automation. The photo server's /display page is embedded in an HA dashboard as a webpage card, making it accessible from any HA client. But the real value is in the REST API integrations.

Fully Kiosk Browser exposes a REST API for remote control, and Home Assistant can call it. This means we can create automations like: reload the slideshow page at 3 AM daily (to pick up any new photos), wake the tablet screen when a specific person arrives home, or turn off the display during movie time. Long-lived access tokens handle authentication between HA and the Fully Kiosk API.

Reliability Through Simplicity

The photo server runs as a systemd service called immich-dashboard with automatic restart on failure. In six months of operation, it has needed zero manual intervention. When the server VM reboots after updates, the service starts automatically, re-polls the Immich album, rebuilds the shuffle deck, and starts serving photos.

The zero-dependency Python approach has proven its worth. We have upgraded the host OS, updated Python minor versions, and restarted Docker containers without ever needing to fix the photo server. There is no virtual environment to maintain, no package version conflicts to resolve, and no requirements.txt to keep up to date. The server is a single file that runs on any Python 3.6+ installation.

# /etc/systemd/system/immich-dashboard.service
[Unit]
Description=Immich Dashboard Photo Server
After=network.target

[Service]
Type=simple
ExecStart=/usr/bin/python3 /opt/immich-dashboard/server.py
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

What We Would Do Differently

If we were starting over, we would add EXIF-based date display as an overlay on the slideshow, showing when each photo was taken. Family photos carry more meaning when you know it was from three summers ago rather than last Tuesday. We would also add a simple web UI for managing which album the server polls, rather than hardcoding it.

The motion detection sensitivity on Fully Kiosk occasionally triggers from sunlight changes through a window. A more robust approach would combine the camera motion detection with a Home Assistant presence sensor, only waking the screen when someone is actually in the room rather than when clouds move.

The Result

The display has quietly become one of the most-used things in the house. Family members regularly add photos from trips, holidays, and everyday moments. Visitors comment on it constantly. The combination of intentional curation, shuffle-deck rotation, and motion-activated wake means the display always feels fresh and never wastes power showing photos to an empty room.

The entire pipeline runs on local hardware with no cloud services, no subscriptions, and no accounts to manage. If our internet goes down, the photo display keeps working. That kind of resilience is hard to find in commercial alternatives, and it is one of the best arguments for building things yourself.

Tech Stack

Photo Management Immich (Docker Compose: server, ML, Redis, PostgreSQL)
Photo Server Python 3 stdlib HTTP server (zero dependencies, single file)
Home Automation Home Assistant with long-lived tokens
Tablet Display Samsung Galaxy Tab A9+ with Fully Kiosk Browser PLUS
TV Display LG TV built-in web browser
Service Management systemd (immich-dashboard.service, auto-restart)
Photo Upload Immich mobile app (iPhone, manual curation)
← Back to Blog Next: Building lakeforestcomputer.com →

Want us to build something like this for you?

Get in Touch