Wydaje się prosty temat, nieprawdaż? Okazuje się że nie 🙂 Aby nadać trochę kontekstu, oto moje „środowisko”:
- Tablet Samsung Tab A9+ powieszony na ścianie
- Ekran 11″ 1920×1200 16:10
- Dedykowany dashboard Home Assistant wyświetlany przez aplikację a nie stronę internetową
- https://github.com/NemesisRE/kiosk-mode – wykorzystane do ograniczenia HomeAssistant do wyświetlania tylko i wyłącznie dedykowanego dashboardu.
- Fully Single App – polecana w internetach do blokowania w „trybie kiosku”, gdzie niemożliwa jest nawigacja poza wybraną aplikację. W ten sposób użytkownik tabletu może nawigować tylko po dashboardzie Home Assistant i nigdzie indziej.

Ogólnie powyższy układ zasługuje na osobny wpis. Tutaj skupię się jedynie na potencjalnie trywialnym aspekcie jakim jest wyświetlanie zdjęć, gdy tablet nie jest używany. Zamysł jest taki, że tablet przez cały dzień funkcjonuje jako ramka zdjęć, lecz gdy dotknie się ekranu, pojawia się dashboard HomeAssistant, gdzie można robić różne rzeczy, natomiast w nocy sie wyłącza.
Podejście numer jeden
Na samym początku pomyślałem, że użyję wbudowanej w Fully Single App funkcji „screen-saver”, lecz ten pomysł szybko odpadł z prostego powodu – w domyślnej konfiguracji „screen-saver” nie reaguje na zdarzenia związane z zasilaniem.
Otóż codziennie o 22:30 od tabletu odłączane jest zasilanie po to, aby przez noc tablet pochodził trochę na baterii (w ten sposób mam nadzieję na wydłużenie życia baterii). Robię to też po to, aby po prostu tablet nie rozświetlał mieszkania gdy wszyscy idą już spać. Niestety wygaszacz ekranu wbudowany w Fully Kiosk App nie wygasza ekranu na odłączanym zasilaniu i raz że przez noc drenuje sporą część baterii, a dwa – świeci w mieszkaniu.
Podejście numer dwa
Podejście numer dwa zakładało użycie wbudowanego w system Android wygaszacza ekranu. Aby móc z tego skorzystać z włączonym trybem kiosku w Fully Single App, musiałem dodać aplikację do wyjątków; jak sama nazwa aplikacji wskazuje, domyślnie Fully Single App działa w taki sposób, że pozwala na działanie tylko jednej aplikacji, ale opcji jest multum i znalazłem tam możliwość robienia wyjątków.
I ogólnie to rozwiązanie działalo całkiem nieźle – tablet gasł po odłączeniu zasilania, wygaszacz właczał się na powrót poprawnie gdy zasilanie wracało. Niestety i to rozwiązanie miało mankament – gdy z tabletu nie korzystało się przez kilka minut lub dłużej (czyli realistycznie, za każdym razem), działający w tle HomeAssistant był „zamrażany”, w efekcie czego po dotknięciu ekranu, początkowo na sekundę – dwie pokazywał się stary widok Home Assistant z dawno nieaktualnymi danymi, który nie reagował na dotknięcia. Po upływie tego czasu, ekran się przeładowywał i wszystko wracało do normy. Ogólnie dało się z tym żyć, ale było dość uciążliwe. I chyba nie tylko ja mam z tym problem:
https://community.home-assistant.io/t/ha-dashboard-keep-your-tablet-awake-on-android-14/793877
Podejście numer trzy, czyli podejście numer jeden bis
Mając w pamięci, że wygaszacz ekranu z Fully nie zamrażał dashboardu HomeAssistant, zdecydowałem się na ponowienie prób z tą aplikacją. W samym HomeAssistant który wyłącza gniazdko o 22:30, zmodyfikowałem automatyzację tak, aby poza wyłaczeniem gniazdka, wyłączała również wygaszacz oraz ekran. No i to okazało się działać tak jak powinno – ekran i cały tablet gasły gdy trzeba, a gdy dotknęło się ekranu, dashboard HomeAssistant pojawiał się od razu aktualny i responsywny. Nie było to jednak wcale trywialne, a tutaj automatyzacja tej procedury:
alias: Wyłączanie gniazdka w przedpokoju / tabletu wieczorem
description: ""
triggers:
- at: "22:30:00"
trigger: time
conditions:
- condition: state
entity_id: input_boolean.tryb_wakacji
state: "off"
actions:
- action: switch.turn_off
metadata: {}
data: {}
target:
entity_id:
- switch.0xa4c138e652c8539f
- delay:
hours: 0
minutes: 0
seconds: 15
milliseconds: 0
- action: switch.turn_off
metadata: {}
data: {}
target:
entity_id:
- switch.pmr_tab_a9_ekran
- switch.pmr_tab_a9_wygaszacz_ekranu
- action: number.set_value
metadata: {}
data:
value: "0"
target:
entity_id: number.pmr_tab_a9_minutnik_wlaczenia_wygaszacza_ekranu
- action: number.set_value
metadata: {}
data:
value: "30"
target:
entity_id: number.pmr_tab_a9_minutnik_wylaczania_ekranu
mode: single
Jak widać, najpierw wieczorem wyłączam gniazdko, potem czekam 15 sekund (aż tablet „pogodzi się z faktem” że jest odłączony od prądu. Bez tego ekran nie zawsze się wyłaczał. Z całą pewnością można czekać krócej, ale 15s w tym scenariuszu nikomu nie przeszkadza), potem wyłaczam zarówno wygaszacz ekranu jak i sam ekran oraz ustawiam dwa timery. Wszystkie te rzeczy są konieczne żeby ekran faktycznie pozostał wyłączony i wszystkie te operacje trzeba odwrocić drugą automatyzacją rano, żeby rano włączyć i ekran pozostał wtedy włączony.
Uf, skomplikowane ale przynajmnie działa. Prawda? PRAWDA? No tak i nie 🙂 Zachowanie tabletu było w 100% poprawne, ale pojawił się inny problem. Dwa ślubne zdjęcia wraz z moją małżonką nie wyświetlały się poprawnie. Są to czarno-białe zdjęcia i wyświetlały się jako negatyw, co wyglądało dość upiornie. Co ciekawe, inne czarno-białe zdjęcia z tej samej sesji wyświetlały się poprawnie. Próbowałem te same zdjęcia zapisać w innej rozdzielczości w nadziei, że być może coś to zmieni, lecz niestety bez skutku. Wygląda na babol w Fully Single App.
Ze wszystkimi dotychczasowymi rozwiązaniami powiązany jest jeszcze jeden problem – wyświetlane zdjęcia przechowywane są na tablecie, więc żeby dorzucić jakąś nową fotkę, musiałem trzymać na tablecie odpaloną aplikację, która robiła serwer http z dostępem do pamięci wewnętrznej tabletu. Ta aplikacja mimo że potrafi działać w tle, często była ubijana, a wtedy musiałem odblokowywać kiosk, włączać aplikację… ogólnie sporo manualnych kroków po to tylko żeby dorzucić jedno nowe zdjęcie. Dlatego też nastąpiło…
Podejście numer primo ultimo, czyli cztery
Wkurzyłem się i pojechałem po bandzie 🙂 Zamiast wyświetlać zdjęcia z pamięci tabletu, stwierdziłem że postawię (kolejny) serwer HTTP na NASie i poprosiłem ChatGPT o napisanie strony internetowej z JS/PHP, która wyświetli mi to co potrzebuję. Trochę przepychanek z AI o to jak ma działać, trochę bugów, trochę ręcznych poprawek i ostateczny kod (po wycięciu danych wrażliwych) wygląda następująco:
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Galeria z zegarem i temperaturą</title>
<style>
body {
background-color: black;
display: flex;
flex-direction: column;
align-items: center;
height: 100vh;
margin: 0;
overflow: hidden;
color: white;
}
.top-bar {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
background-color: rgba(0, 0, 0, 0.6);
padding: 10px;
font-size: 28px;
font-family: Roboto, sans-serif;
position: absolute;
top: 0;
left: 0;
}
.clock {
padding: 5px;
border-radius: 5px;
}
.temperature {
padding: 5px;
border-radius: 5px;
margin-right: 15px
}
.gallery {
flex-grow: 1;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
position: relative;
margin-top: 50px;
}
.gallery img {
position: absolute;
max-width: 100%;
max-height: 100%;
object-fit: contain;
opacity: 0;
transition: opacity 1s ease-in-out;
}
</style>
</head>
<body>
<div class="top-bar">
<div class="clock" id="clock"></div>
<div class="temperature" id="temperature"></div>
</div>
<div class="gallery">
<img id="photo1" alt="Slideshow Image">
<img id="photo2" alt="Slideshow Image">
</div>
<script>
let images = [];
let currentIndex = 0;
let imgElements = [document.getElementById("photo1"), document.getElementById("photo2")];
let currentImg = 0;
const clockElement = document.getElementById("clock");
const temperatureElement = document.getElementById("temperature");
const weekdays = ["Niedziela", "Poniedziałek", "Wtorek", "Środa", "Czwartek", "Piątek", "Sobota"];
const months = ["Styczeń", "Luty", "Marzec", "Kwiecień", "Maj", "Czerwiec", "Lipiec", "Sierpień", "Wrzesień", "Październik", "Listopad", "Grudzień"];
function fetchImages() {
fetch("gallery.php")
.then(response => response.json())
.then(data => {
if (JSON.stringify(data) !== JSON.stringify(images)) {
images = data;
currentIndex = 0;
if (images.length > 0) showImage();
}
})
.catch(error => console.error("Błąd pobierania zdjęć:", error));
}
function showImage() {
if (images.length === 0) return;
let nextImg = (currentImg + 1) % 2;
imgElements[nextImg].src = "photos/" + images[currentIndex];
imgElements[nextImg].onload = () => {
imgElements[nextImg].style.opacity = 1;
imgElements[nextImg].style.zIndex = 2;
imgElements[currentImg].style.opacity = 0;
imgElements[currentImg].style.zIndex = 1;
currentImg = nextImg;
setTimeout(nextImage, 15000);
};
}
function nextImage() {
currentIndex = (currentIndex + 1) % images.length;
showImage();
}
function updateClock() {
const now = new Date();
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
const day = weekdays[now.getDay()];
const date = now.getDate();
const month = months[now.getMonth()];
const dateString = `${day}, ${date} ${month}`;
clockElement.textContent = `${hours}:${minutes}:${seconds} | ${dateString}`;
}
function fetchTemperature() {
Promise.all([
fetch("<tutaj_wyciety_url_do_encji_z_temperatura_zewnatrz>", {
headers: {
"Authorization": "<tutaj_wyciety_dlugoterminowy_token>"
}
}).then(response => response.json()),
fetch("<tutaj_wyciety_url_do_encji_z_temperatura_wewnatrz>", {
headers: {
"Authorization": "Bearer <tutaj_wyciety_dlugoterminowy_token>"
}
}).then(response => response.json())
])
.then(([outsideData, insideData]) => {
const outsideTemp = outsideData.state ? `${parseFloat(outsideData.state).toFixed(1)}°C` : "Nieznana";
const insideTemp = insideData.state ? `${parseFloat(insideData.state).toFixed(1)}°C` : "Nieznana";
temperatureElement.textContent = `Zewnątrz: ${outsideTemp} | Wewnątrz: ${insideTemp}`;
})
.catch(error => console.error("Błąd pobierania temperatury:", error));
}
setInterval(updateClock, 1000);
setInterval(fetchTemperature, 60000);
fetchImages();
fetchTemperature();
</script>
</body>
</html>
Optymalne? Nie. Piękne? HTML nigdy nie jest piękny 🙂 Ale ten przynajmniej działa! Przy okazji dowiedziałem się o istnieniu czegoś takiego jak CORS – musiałem odpowiednio skonfigurować swoją instancję HomeAssistant by poprawnie formułowała odpowiedzi i przeglądarka akceptowała i wyświetlała temperaturę. A właśnie, o tym nie wspomniałem 🙂 Teraz to nie tylko zdjęcia, ale pasek informacyjny z datą, godziną i temperatuarmi na zewnątrz i wewnątrz! Teraz i ja mogę mieć swój pasek informacyjny! Moja strona co minutę wysyła zapytania REST API do HomeAssistant i podaje informacje na temat rzeczywistej temperatury na zewnątrz i wewnątrz, zmierzone moimi własnymi sensorami. Same przydatne informacje przed wyjściem z domu 🙂 Fajnym aspektem jest również to, że tablet ma proporcje ekranu 16:10 podczas gdy większość zdjęć (ale nie wszystkie) mam w proporcjach 16:9, więc i tak były czarne paski u góry i dołu ekranu.
Oto jak się prezentuje to w akcji:
…tylko zamiast zdjęć, mój folder z memami 🙂 Po jednym dniu testów wygląda na to, że wszystkie problemy rozwiązane, a sam wygaszacz mogę w pełni dostosowywać, bo ostatecznie jest to strona www. No i zdjęcia są trzymane na NASie, więc dodanie nowego zdjęcia to kwestia skopiowania go w odpowiednie miejsce bez żadnych dodatkowych czynności dzięki temu, że jest tam jeszcze skrypt PHP, który na bieżąco odświeża zawartość katalogu i serwuje nowe zdjęcia.
Tak, używam tego dashboardu:
https://github.com/jimmy-landry/HA-Tablet-Dashboard-Config
Nie, nie polecam tego dashboardu 🙂 Straszliwe spaghetti code.
Jakie informacje mogłyby być jeszcze przydatne przy wychodzeniu z domu?