Per-frame palette calibration + sidebar battery indicator
Build and push server image / build-and-push (push) Successful in 40s

Advanced configuration (Configuration tab, collapsed <details> section):
a color picker per ink color (black/white/yellow/red/blue/green),
overriding image_pipeline.DEFAULT_PALETTE_RGB for that frame's actual
panel -- different units can vary enough from the documented
approximations to be worth calibrating once you can compare a rendered
photo against the real hardware. Stored as Frame.palette_rgb (NULL =
default, schema migration v4), threaded through render_frame/
render_placeholder/_quantize_and_pack (which now builds the PIL palette
image per call instead of once at import) so both photos and the
unclaimed/unconfigured placeholder screen respect it. "Reset to
defaults" clears back to NULL. Config-save validates exactly 6 #rrggbb
values, rejecting anything else with a 400.

Also: each frame's sidebar entry now shows its last-reported battery
percent (🔋NN%) next to the name, using the frame_dot's existing
recently-seen indicator conventions -- silent when never reported
(mains-only frames, or before the first report), matching how battery
is hidden everywhere else it's not applicable.

Verified against the same live-shaped database as the SMTP work: the
v3->v4 migration, save/reload/reset round trip through the real HTTP
route, an actual rendered image using a custom palette (confirmed via
its packed panel-code bytes), input validation, and the sidebar badge
against real battery data -- plus the standing legacy-device curl suite.
This commit is contained in:
2026-07-22 01:19:06 -04:00
parent c1c803b497
commit 5b11f2accb
12 changed files with 185 additions and 38 deletions
+1 -1
View File
@@ -85,7 +85,7 @@ def render_asset(client: ImmichClient, frame: Frame, asset_id: str) -> bytes:
logger.warning("Could not fetch faces for asset %s: %s", asset_id, e)
source = Image.open(io.BytesIO(jpeg_bytes))
return render_frame(source, faces=faces, orientation=frame.orientation)
return render_frame(source, faces=faces, orientation=frame.orientation, palette_rgb=frame.palette_rgb)
def battery_estimate_s(frame: Frame) -> int | None: