Skip to content

Instantly share code, notes, and snippets.

@JamesDAdams
Created January 9, 2026 09:58
Show Gist options
  • Select an option

  • Save JamesDAdams/b42720ec720b35ec9eebfc4c1dffad3a to your computer and use it in GitHub Desktop.

Select an option

Save JamesDAdams/b42720ec720b35ec9eebfc4c1dffad3a to your computer and use it in GitHub Desktop.
HaKioskTv Requirement & scripts

Home Assistant Kiosk + Immich Slideshow (Fullscreen Dashboard with Background Slideshow)

This guide shows how to install the HACS kiosk-mode integration to open a Home Assistant dashboard in fullscreen (kiosk) and how to add a script that plays a background slideshow of media from an Immich album.


Prerequisites

  • Home Assistant with access to the File Editor or the config/www/ folder.
  • HACS (Home Assistant Community Store) installed.
  • An Immich account and an Immich API key with album read permission.
  • The Immich album ID you want to show.

1) Install kiosk-mode (HACS)

For the best experience, install the HACS package:

Follow the repository instructions to install via HACS.


2) Open a dashboard in fullscreen (kiosk)

Append ?kiosk to the end of your dashboard URL to open it in fullscreen (kiosk) mode.

Example: https://YOUR_HA_DOMAIN.com/lovelace/DASHBOARD_NAME?kiosk


3) Add the Immich slideshow script to Home Assistant

You will place a small JavaScript file in Home Assistant's www folder.

  1. Open File Editor (or access config/www directly).
  2. Create the folder www/custom if it doesn't exist.
  3. Create a file named:
    • HaKioskTv-script-v33.js
  4. Paste the provided JavaScript code into that file.
  5. Edit the variables described below.

Note: If you already have a script with a different name, adapt the filename and resource path accordingly.


4) Configure the script variables

At the top of HaKioskTv-script-v33.js, update these variables with your values:

const CHANGE_INTERVAL = 60000;      // Time between media items (milliseconds). Example: 60000 = 60s
const ALBUM_ID = "your_album_id";   // Immich album ID (the code after /albums/ in the album URL)
const IMMICH_BASE_URL = "https://immich.example.com/api/"; // Must end with /api/
const IMMICH_API_KEY = "your_immich_api_key"; // Needs album read permission

Where to find values:

  • ALBUM_ID: In the album URL, e.g. https://immich.example.com/albums/<ALBUM_ID>
  • IMMICH_API_KEY: Create/copy from Immich user settings → API keys
    Example: https://IMMICH_DOMAIN.com/user-settings?isOpen=api-keys

5) Add the script as a dashboard resource

  1. Enable Advanced Mode in your Home Assistant profile if not already enabled: https://YOUR_HA_DOMAIN.com/profile/general
  2. Open the dashboard you want to modify.
  3. Click Edit (pencil), then click the three dots ⋯ → Manage resources.
  4. Click Add resource.
  5. In URL, enter:
    • /local/custom/HaKioskTv-script-v33.js (If you used a different filename or folder, adjust the path.)
  6. Set Type to the appropriate option (usually: JavaScript Module).
  7. Save the resource.

Note: /local/ maps to the config/www/ folder.


6) Open the dashboard in kiosk mode

After adding the resource, open the dashboard with the kiosk parameter:

https://YOUR_HA_DOMAIN.com/lovelace/DASHBOARD_NAME?kiosk

The script should load and display the Immich album media as a background slideshow.


Troubleshooting & Tips

  • Replace placeholder values (YOUR_HA_DOMAIN.com, IMMICH_DOMAIN.com, DASHBOARD_NAME, your_album_id, your_immich_api_key) with real values.
  • Ensure Home Assistant can reach your Immich instance (same network or public URL).
  • If the script fails to load, open the browser console (F12) to check errors (CORS, 401/403 for invalid API key, 404 for wrong resource path).
  • Use an Immich API key with the minimal permission required (album read) for security.
  • If hosting behind a reverse proxy or using SSL, ensure the Immich URL scheme (http/https) matches and CORS is correctly configured.

Quick checklist

  • Install HACS and add NemesisRE/kiosk-mode
  • Place HaKioskTv-script-v33.js in config/www/custom/
  • Edit CHANGE_INTERVAL, ALBUM_ID, IMMICH_BASE_URL, IMMICH_API_KEY
  • Add /local/custom/HaKioskTv-script-v33.js as a dashboard resource
  • Open https://YOUR_HA_DOMAIN.com/lovelace/DASHBOARD_NAME?kiosk

If you want, I can:

  • provide a minimal example skeleton of HaKioskTv-script-v33.js you can paste into the file, or
  • review and clean up the existing script if you paste its code here.
console.log("custom-ha-script.js chargé v33 - Shuffle Images + Vidéos (dashboards filtrés)");
// === 0) Allowed dashboards ===
// Put here the dashboard IDs as they appear in the URL:
// e.g.: http://.../lovelace-tablette/0 → "lovelace-tablette"
const DASHBOARD_WHITELIST = [
"dashboard-test",
"lovelace-salon"
];
// Detect the current dashboard ID from the URL
function getCurrentDashboardId() {
const path = window.location.pathname || "";
// Example path: "/lovelace-tablette/0"
const parts = path.split("/").filter(Boolean);
// The first segment after the root is usually the dashboard ID
return parts.length > 0 ? parts[0] : null;
}
const currentDashboardId = getCurrentDashboardId();
console.log("Dashboard courant détecté :", currentDashboardId);
// If the current dashboard is not in the whitelist, stop everything
if (!currentDashboardId || !DASHBOARD_WHITELIST.includes(currentDashboardId)) {
console.log("Dashboard non autorisé pour le slideshow Immich, script arrêté.");
} else {
console.log("Dashboard autorisé, activation du slideshow Immich.");
const CHANGE_INTERVAL = 60000; // 60 seconds
const ALBUM_ID = 'YOUR_ALBUM_ID';
const IMMICH_BASE_URL = 'https://immich.domain.com/api';
const IMMICH_API_KEY = 'YOUR_IMMICH_API_KEY';
let currentBlobUrl = null;
let currentVideoElement = null;
let currentTimeoutId = null;
let albumMediaAssets = [];
let playlist = [];
let playlistIndex = 0;
let haVisible = true;
// 1) Function to make HA transparent
function setHATransparency(isTransparent) {
const main = document.querySelector("body");
if (isTransparent) {
document.documentElement.style.setProperty("--lovelace-background", "transparent");
document.documentElement.style.setProperty("--ha-card-background", "rgba(255, 255, 255, 0.1)");
if (main) main.style.backgroundColor = "transparent";
} else {
document.documentElement.style.setProperty("--lovelace-background", "black");
}
}
// 2) Function to hide/show home-assistant
function toggleHomeAssistantVisibility() {
const haRoot = document.querySelector("home-assistant");
if (!haRoot) {
console.warn("home-assistant introuvable dans le DOM");
return;
}
haVisible = !haVisible;
if (haVisible) {
haRoot.style.visibility = "visible";
haRoot.style.opacity = "1";
console.log("🔲 home-assistant visible");
} else {
haRoot.style.visibility = "hidden";
haRoot.style.opacity = "0";
console.log("⬛ home-assistant masqué");
}
}
// --- Playlist logic ---
function shuffleArray(arr) {
const a = arr.slice();
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
}
async function loadAlbumIfNeeded() {
if (albumMediaAssets.length > 0) return;
try {
console.log("Chargement album Immich…");
const res = await fetch(`${IMMICH_BASE_URL}/albums/${ALBUM_ID}`, {
method: "GET",
headers: {
"x-api-key": IMMICH_API_KEY,
"Accept": "application/json"
}
});
if (!res.ok) {
console.error("Erreur API Immich (album) :", res.status, res.statusText);
return;
}
const album = await res.json();
albumMediaAssets = (album.assets || []).filter(a => a.type === "IMAGE" || a.type === "VIDEO");
if (!albumMediaAssets.length) {
console.warn("Aucune image ou vidéo dans cet album.");
return;
}
console.log("Album chargé, nombre de médias (image+vidéo) :", albumMediaAssets.length);
playlist = shuffleArray(albumMediaAssets);
playlistIndex = 0;
} catch (err) {
console.error("Erreur lors de la récupération de l'album Immich :", err);
}
}
async function getNextAssetFromPlaylist() {
await loadAlbumIfNeeded();
if (!albumMediaAssets.length) return null;
if (playlistIndex >= playlist.length) {
playlist = shuffleArray(albumMediaAssets);
playlistIndex = 0;
console.log("Nouvelle playlist shuffle générée.");
}
const asset = playlist[playlistIndex];
playlistIndex += 1;
console.log(
"Média depuis playlist :",
asset.type,
asset.id,
asset.originalFileName,
`(${playlistIndex}/${playlist.length})`
);
return asset;
}
// --- Display ---
function cleanupCurrentMedia() {
if (currentTimeoutId) {
clearTimeout(currentTimeoutId);
currentTimeoutId = null;
}
if (currentBlobUrl) {
URL.revokeObjectURL(currentBlobUrl);
currentBlobUrl = null;
}
document.body.style.backgroundImage = "";
if (currentVideoElement) {
currentVideoElement.pause();
currentVideoElement.remove();
currentVideoElement = null;
}
}
function displayImage(blobUrl) {
setHATransparency(false);
document.body.style.backgroundImage = `url("${blobUrl}")`;
document.body.style.backgroundSize = "contain";
document.body.style.backgroundPosition = "center";
document.body.style.backgroundRepeat = "no-repeat";
document.body.style.backgroundColor = "black";
}
function displayVideo(blobUrl) {
setHATransparency(true);
const video = document.createElement("video");
video.src = blobUrl;
video.autoplay = true;
video.muted = true;
video.playsInline = true;
video.loop = false;
Object.assign(video.style, {
position: "fixed",
top: "0",
left: "0",
width: "100%",
height: "100%",
objectFit: "contain",
backgroundColor: "black",
zIndex: "-1",
pointerEvents: "none"
});
video.addEventListener("error", (e) => {
console.error("Erreur de lecture vidéo :", e, video.error);
});
document.body.appendChild(video);
currentVideoElement = video;
console.log("Vidéo ajoutée en background, src =", blobUrl);
return video;
}
// --- Progress bar ---
function startProgress(duration) {
let container = document.getElementById("bg-progress-container");
if (!container) {
container = document.createElement("div");
container.id = "bg-progress-container";
Object.assign(container.style, {
position: "fixed",
left: "0",
bottom: "0",
width: "100%",
height: "4px",
background: "rgba(0, 0, 0, 0.4)",
zIndex: "9999"
});
const bar = document.createElement("div");
bar.id = "bg-progress-bar";
Object.assign(bar.style, {
width: "0%",
height: "100%",
background: "#00bcd4",
transition: "none"
});
container.appendChild(bar);
document.body.appendChild(container);
}
const bar = document.getElementById("bg-progress-bar");
bar.style.transition = "none";
bar.style.width = "0%";
void bar.offsetWidth;
bar.style.transition = `width ${duration}ms linear`;
bar.style.width = "100%";
}
async function showNextMedia() {
console.log("showNextMedia() appelé");
const asset = await getNextAssetFromPlaylist();
if (!asset) {
console.warn("Impossible de choisir un asset depuis la playlist. Nouveau essai dans 5s.");
setTimeout(showNextMedia, 5000);
return;
}
const isVideo = asset.type === "VIDEO";
console.log("=== Nouveau média ===", isVideo ? "VIDEO" : "IMAGE", asset.id);
const url = isVideo
? `${IMMICH_BASE_URL}/assets/${asset.id}/original`
: `${IMMICH_BASE_URL}/assets/${asset.id}/thumbnail?size=preview`;
console.log("Fetch asset :", url);
try {
const res = await fetch(url, {
headers: { "x-api-key": IMMICH_API_KEY }
});
if (!res.ok) {
console.error("Erreur API Immich (asset) :", res.status, res.statusText);
setTimeout(showNextMedia, 5000);
return;
}
const blob = await res.blob();
console.log("Blob reçu : type =", blob.type, ", taille =", blob.size, "bytes");
cleanupCurrentMedia();
currentBlobUrl = URL.createObjectURL(blob);
if (isVideo) {
const video = displayVideo(currentBlobUrl);
const playPromise = video.play();
if (playPromise && typeof playPromise.then === "function") {
playPromise
.then(() => {
console.log("play() résolu, la vidéo devrait être en cours de lecture");
})
.catch((err) => {
console.error("Erreur lors de play() sur la vidéo :", err);
});
}
video.addEventListener("loadedmetadata", () => {
let durationSec = video.duration;
if (!isFinite(durationSec) || durationSec <= 0) {
durationSec = CHANGE_INTERVAL / 1000;
}
const durationMs = durationSec * 1000;
console.log("Durée vidéo détectée :", durationSec, "sec");
startProgress(durationMs);
currentTimeoutId = setTimeout(showNextMedia, durationMs + 500);
});
video.addEventListener("ended", () => {
console.log("Vidéo terminée, passage immédiat au média suivant.");
if (currentTimeoutId) {
clearTimeout(currentTimeoutId);
currentTimeoutId = null;
}
showNextMedia();
});
} else {
displayImage(currentBlobUrl);
startProgress(CHANGE_INTERVAL);
currentTimeoutId = setTimeout(showNextMedia, CHANGE_INTERVAL);
}
console.log("Média affiché :", asset.type, "id :", asset.id);
} catch (e) {
console.error("Erreur lors du téléchargement de l'asset :", e);
setTimeout(showNextMedia, 5000);
}
}
// --- Keyboard controls ---
document.addEventListener("keydown", (e) => {
if (e.key === "ArrowRight" || e.code === "ArrowRight" || e.keyCode === 39) {
console.log("➡️ Touche ArrowRight détectée → passage au média suivant");
e.preventDefault();
showNextMedia();
}
if (e.key === "ArrowDown" || e.code === "ArrowDown" || e.keyCode === 40) {
console.log("⬇️ Touche ArrowDown détectée → toggle home-assistant");
e.preventDefault();
toggleHomeAssistantVisibility();
}
});
// --- Reliable start ---
if (document.readyState === "complete" || document.readyState === "interactive") {
console.log("DOM déjà prêt – démarrage slideshow Immich…");
setTimeout(showNextMedia, 0);
} else {
document.addEventListener("DOMContentLoaded", () => {
console.log("DOMContentLoaded – démarrage slideshow Immich…");
showNextMedia();
});
}
} // end of allowed dashboard if-block
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment