DEV Community

Michael Brewer
Michael Brewer

Posted on Originally published at michaelbrewer.me

The Photo Frame I Didn't Have Time to Fix

A seven-year-old Raspberry Pi project, a Google API that stopped doing the one thing it needed, and how AI agents got it running every day on my wall.

photoframe is Henric Andersson's Raspberry Pi project that pulls random photos from Google Photos and shows them like a picture frame. It's a good piece of software. Then in 2025 Google cut the Photos Library API down to photos an app uploaded itself, and a frame that reads your library has nothing left to read.

I self-host Immich. My photos are already there. So the fix was obvious on paper: teach photoframe to talk to Immich.

The fix was not obvious on my calendar. I'm far too busy to spin up a whole new photo service, build an API for it, test the app on a Pi Zero, a Pi 3B+, a Pi 4 and a Pi 5, check it on displays from 800x480 up to 1920x1080, and then build the stability controls a thing on the wall needs. Without AI that list is wishful thinking. With it, the frame has been running every day without a hitch.

Here's how it got there, including the parts where the agents went further than I told them to.

What I started with

Upstream already had a Python 3 branch. Henric's commit on it reads "Major update to python 3, still many things left to test," and it never made it to master. On top of that, Raspberry Pi OS Bookworm dropped tvservice, which photoframe used to find out what display it was plugged into. So it wasn't only a new photo source. The base had to come up to the current OS first.

The first weekend

The first Immich work happened over a weekend in September 2025. The shape of it hasn't changed much since:

  • Auth is an API key in the x-api-key header instead of Google's OAuth dance.
  • Albums act as photoframe's "keywords," so you pick albums and the frame rotates through them.
  • Immich gets its own /immichconfig route. The existing config routes stay exactly as they were.

That last point was a rule, written into the project's CLAUDE.md in capital letters: "NEVER modify existing routes for Immich features." The work ran as roles: an architect agent wrote phased roadmaps, I approved them, a developer agent built, and a QA agent tested. The roadmaps carried their own rules, like "DO NOT make unauthorized git commits" and "Product Manager (User) approval required before implementation." I was the Product Manager.

The QA agent's first baseline report opened with "CRITICAL FAILURE IDENTIFIED." Good. That's its job.

The developer agent, meanwhile, decided the Flask error handling in server.py needed help and added handlers there. That is an existing file the rule said not to touch. The commit that followed is titled "Revert unauthorized changes." The rule file said where the boundary was. The revert is what made it a boundary.

The push that went too far

In April 2026 I came back for the real release push, and an automated session did something I still find instructive. In one run it rebased the Python 3 branch onto master, pushed master, created a v3.0.0 tag and release, created a dev branch, and forked the Raspberry Pi image builder. None of it had been tested on a Pi. It also closed my open pull request to the upstream project by deleting the branch behind it.

None of those steps is crazy on its own. Together they're a release nobody had run.

The answer wasn't to trust the agents less in some vague way. It was a written rollout plan with a damage assessment table, master reset back to the last known good commit, and one gate that everything since has had to pass:

Nothing gets pushed, tagged, released, or sent upstream until it's been tested on Pi hardware.

A pull request became the vetting gate for the release flow. A Pi Zero W with a 1366x768 laptop panel ran the full image before rc1 got tagged on April 18. That rule has paid for itself more than any other in this project. I wrote about why a rule like that has to live in a mechanism instead of a prompt in Prompts Are Requests. Hooks Are Law.

What it does now

  • Immich, sized for the hardware. The frame asks Immich for a preview image first and only steps up to full size or the original when /proc/meminfo and ImageMagick's limits say the Pi can handle it. HEIC works. There's an album picker in the web UI instead of typing album names.
  • Displays without tvservice. If tvservice is on the system, it's used. If not, the frame falls back to the framebuffer (fbset, /sys/class/graphics) with health checks and safe defaults. On Bookworm desktop images it stops lightdm so it can own the screen.
  • Memory. On a Pi 3B+ driving an 800x480 panel, an out-of-memory fix took peak memory from about 1061 MB to about 494 MB.
  • Network trouble. Downloads retry with exponential backoff and jitter, six attempts between 5 and 60 seconds, and permanent HTTP errors are kept separate from temporary ones. If an album refresh fails, the frame keeps the last good album list and tries again in a minute instead of going blank.
  • Clean shutdown. SIGTERM and SIGINT blank the display instead of leaving a frozen photo on screen. Credentials no longer show up in logs.
  • A real image. A fork of pi-gen builds a headless Lite image. You flash it, put your wifi details in wifi-config.txt on the boot partition, and the frame comes up with no SSH needed. CI builds that image under QEMU. There's also a migration script for people coming from the original photoframe.

Measured against the original project's master, the fork touches 65 files, with 4,563 lines added and 1,618 removed. On GitHub that's 40 merged pull requests and 29 closed issues so far.

Review until it's boring

v3.0.0-rc2 was tagged October 1. Its remediation pull request went through an independent reviewer four times, and every pass got answered with a commit, not a comment saying it would be handled later. Pi 4 runs with a 1920x1080 monitor went about 17 hours at a stretch.

Where it stands

It's still a release candidate, and the issue tracker is honest about what's left. Cached photos don't show after a restart with no network. Rotation does nothing under KMS. Support for Immich's v3 API is built but waiting for 3.1. Config backup and restore and image prioritization (recent, by date taken, favorites) sit in open pull requests. There's a pull request offering the Python 3 work back to upstream, open since April.

None of that stops the frame on my wall from doing its job every day.

What made the difference wasn't a model writing Python faster than I can. It was the rules around the models: a route they couldn't touch, a revert when they did, and a gate that says nothing ships until a real Pi has run it. Each of those came from watching an agent go somewhere it shouldn't. That's not a cost of using agents. It's the work.

The code is at github.com/dev-brewery/photoframe. If you run Immich and have a Pi in a drawer, the rc2 image is on the releases page.

Top comments (0)