English · Deutsch
A fullscreen kiosk browser (Chromium/WebView) for Android wall tablets that shows an FHEM dashboard and integrates the tablet as an FHEM device over MQTT.
- 🔋 Battery & charging – level, charging state, temperature
- 🔊 Text-to-speech – FHEM sends text, the tablet speaks it
- 🔈 Sounds – play stored sound files in the background
- 🖥️ Screen control – on/off (overlay) and brightness
- 🚶 Presence / motion – camera motion detection wakes the display (no image leaves the device)
- 🗂️ Up to 3 dashboards – named tabs, switchable from the menu; the default dashboard is always shown again when the screen wakes up
- 🌐 Remote control – switch dashboard or URL, reload, clear cache – all via MQTT
- ☰ Menu – swipe in from the left: MQTT status, dashboard list, actions, settings (optional PIN), about, exit
- 🌍 Bilingual – English (default) / German
Everything runs in a persistent foreground service with auto-reconnect to the MQTT broker.
Requires Android Studio (Ladybug or newer) with Android SDK 35.
- In Android Studio: File → Open → select this folder (
fullscreendy). - Wait for the Gradle sync (downloads AGP 8.7, Kotlin 2.1, dependencies).
- Run on a device/emulator (Android 9+, API 28 – also runs on Fire OS 7 / Fire HD tablets), or build an APK: Build → Build Bundle(s)/APK(s) → Build APK(s).
./gradlew assembleDebug # app/build/outputs/apk/debug/
./gradlew assembleRelease # signed release (needs keystore.properties)
./gradlew assembleDev -PdevNum=1 # pre-release build, version 0.x.y-dev.1One build serves every channel – the same APK that lands in the GitHub release is what
F-Droid rebuilds and verifies. The app never downloads or installs APKs itself, so it
does not ask for REQUEST_INSTALL_PACKAGES; About only checks for a newer version and
links to the release page. Details in fdroid/README.md.
Without keystore.properties no signing config is created at all, so the build still
succeeds and produces an unsigned APK.
Prebuilt signed APKs are on the Releases page.
- Launch the app. As no URL is configured yet, Settings open right away.
- Enter:
- Dashboards: Dashboard 1 always exists; further dashboards (max 3) are added
with the “+” tab. Per dashboard: display name (shown in the menu),
URL (e.g.
http://192.168.1.10:8083/fhem/floorplan/Home), an optional login (user/password for a password-protected dashboard, e.g. FHEMbasicAuth– sent as HTTP Basic Auth, cleaner thanuser:pass@in the URL) and allow self-signed certificates (enable if that dashboard uses HTTPS with a self-signed/invalid certificate, e.g. local FHEM – off by default; only use on your own LAN). One dashboard is the default dashboard (star): it is shown on start and again every time the screen wakes up. - Connection: MQTT host/port (+ optional user/password/TLS), base topic
(default
fhem/tablet) and device ID (defaulttablet1) - System: language, PIN protection (off by default) and admin PIN
(default
0000)
- Dashboards: Dashboard 1 always exists; further dashboards (max 3) are added
with the “+” tab. Per dashboard: display name (shown in the menu),
URL (e.g.
- Settings save themselves ~1 s after the last change; go back → the dashboard is shown.
Allow the camera and notification permissions on first launch.
Menu / back to settings: swipe in from the left edge → menu with MQTT status, the dashboard list (tap to switch), Reload, Clear cache, Screen off, Settings (PIN if enabled), About (version, update, GitHub), Exit app.
The app is deliberately not a launcher/home replacement – the tablet stays usable normally. For autostart after boot: enable Settings → System → “Start on boot”. On some devices (Xiaomi, Huawei, …) additionally allow the OEM autostart for FullScreendy in the Android settings. For full lockdown, consider Android screen pinning.
Base: <base-topic>/<device-id>, e.g. fhem/tablet/tablet1.
The tablet reports (retained):
| Topic | Values |
|---|---|
…/status |
online / offline (last will) |
…/battery |
0–100 |
…/charging |
on / off |
…/plug |
ac / usb / wireless / none |
…/batteryTemp |
e.g. 24.5 |
…/motion |
on / off |
…/presence |
present / absent |
…/screen |
on / off |
…/brightness |
0–100 or auto |
…/volume |
0–100 (media volume) |
…/url |
currently loaded URL |
…/dashboard |
number of the visible dashboard (1–3) |
…/dashboardName |
its display name |
…/ip |
IPv4 address |
…/appVersion |
app version, e.g. 0.2.0 |
…/androidVersion |
e.g. 13 (SDK 33) |
The tablet accepts commands (…/cmd/…):
| Topic | Payload | Effect |
|---|---|---|
cmd/tts |
any text | speaks the text (aliases: say, speak) |
cmd/mediaplay |
folder/sound.mp3 |
plays a stored sound file (aliases: media, play) |
cmd/mediastop |
(any) | stops playback |
cmd/url |
URL | loads another page and reports it as the url reading |
cmd/dashboard |
1–3 or name |
switches to that dashboard (alias: db) |
cmd/reload |
(any) | reloads the page |
cmd/clearcache |
(any) | clears the browser cache |
cmd/screen |
on / off |
physically wakes the display (on) or black overlay (off) |
cmd/screensaver |
on / off |
like screen, inverted |
cmd/brightness |
0–100 or auto |
brightness (full range with “Allow brightness control”) |
cmd/volume |
0–100 |
media volume (TTS/sounds) |
cmd/lock |
(any) | locks the screen (needs device admin) |
cmd/unlock |
(any) | wakes & dismisses the (insecure) lock screen |
cmd/vibrate |
duration in ms (empty = 200) | vibration feedback, max 5000 ms |
Sounds: under Settings → Sounds pick the folder holding your sound files. The app
is granted exactly that folder through Android's folder picker and needs no storage
permission at all. Then play via cmd/mediaplay ding.mp3; subfolders work with /.
http… URLs are streamed. Without a folder selection the app uses its own directory
(Android/data/de.kewl.fullscreendy/files/sounds).
Requires an MQTT server in FHEM (MQTT2_SERVER) or an MQTT2_CLIENT connected
to your broker (e.g. mosquitto).
As soon as the tablet publishes for the first time, FHEM suggests an MQTT2_DEVICE
via autocreate. Alternatively, manually:
define fullscreendy MQTT2_DEVICE
attr fullscreendy readingList fhem/tablet/tablet1/status:.* status \
fhem/tablet/tablet1/battery:.* battery \
fhem/tablet/tablet1/charging:.* charging \
fhem/tablet/tablet1/plug:.* plug \
fhem/tablet/tablet1/batteryTemp:.* batteryTemp \
fhem/tablet/tablet1/motion:.* motion \
fhem/tablet/tablet1/presence:.* presence \
fhem/tablet/tablet1/screen:.* screen \
fhem/tablet/tablet1/brightness:.* brightness \
fhem/tablet/tablet1/volume:.* volume \
fhem/tablet/tablet1/url:.* url \
fhem/tablet/tablet1/ip:.* ip \
fhem/tablet/tablet1/appVersion:.* appVersion \
fhem/tablet/tablet1/androidVersion:.* androidVersion
Set commands must be added manually (replace fullscreendy with your device name):
attr fullscreendy setList screenOn:noArg fhem/tablet/tablet1/cmd/screen on \
screenOff:noArg fhem/tablet/tablet1/cmd/screen off \
reload:noArg fhem/tablet/tablet1/cmd/reload 1 \
clearCache:noArg fhem/tablet/tablet1/cmd/clearcache 1 \
lock:noArg fhem/tablet/tablet1/cmd/lock 1 \
unlock:noArg fhem/tablet/tablet1/cmd/unlock 1 \
mediaStop:noArg fhem/tablet/tablet1/cmd/mediastop 1 \
brightness:slider,0,1,100 fhem/tablet/tablet1/cmd/brightness $EVTPART1 \
volume:slider,0,1,100 fhem/tablet/tablet1/cmd/volume $EVTPART1 \
url:textField fhem/tablet/tablet1/cmd/url $EVTPART1 \
mediaPlay:textField fhem/tablet/tablet1/cmd/mediaplay $EVTPART1 \
say:textField {my @a=split(" ",$EVENT);; shift(@a);; return "fhem/tablet/tablet1/cmd/tts ".join(" ",@a)}
attr fullscreendy stateFormat online battery %
$EVTPART1 is enough for values without spaces (number, URL, file path).
setList splits arguments word by word, so for free text use a Perl expression that
takes everything after the first space of $EVENT:
attr <device> setList say:textField {my @a=split(" ",$EVENT);; shift(@a);; return "fhem/tablet/tablet1/cmd/tts ".join(" ",@a)}
Or publish directly (<IODev> = your MQTT2_SERVER/CLIENT):
set MQTT2_Broker publish fhem/tablet/tablet1/cmd/tts Good morning, it is 8 degrees
# Light on when motion is detected
define n_hall_light notify tablet1:motion:.on set HallLight on
# Play a doorbell sound (file in the sound folder)
set MQTT2_Broker publish fhem/tablet/tablet1/cmd/mediaplay doorbell.mp3
Under Settings → Behavior:
- Pull down to reload: only a downward pull that starts at the very top of the page reloads (with the familiar spinner). Scrolling up simply stops at the top of the page – no accidental reload while scrolling.
- Motion sensitivity (slider) for the camera detection.
- Wake on sound (microphone) with sound sensitivity – loud ambient noise wakes the display (loudness only, no recording).
- Test indicators: with motion/sound detection enabled, green dots show each detection live (the device vibrates briefly). Save first, then wave / make noise.
- Robust against light changes: motion detection subtracts the global brightness change and requires several frames – uniform flicker (TV, lighting) does not trigger, local movement does. If it still reacts, lower the motion sensitivity.
Under Settings → Display:
- Keep screen always on is off by default.
- Screen dimming (default 60 s): dims to black via an overlay after inactivity
instead of the Android timeout turning the display off. The screen stays
technically on so camera/microphone keep running and motion/sound wake
reliably – and there is no wallpaper/lock screen flash on wake. Touch or
any motion clears the dim immediately and restarts the timer.
0 s= never. (When wake-on-motion/sound is enabled, the screen stays on automatically.) - Turn screen off (default 0 = never): after even longer inactivity, fully turn
off the display (via device admin). Afterwards only touch/power/
cmd/screen onwakes it – motion no longer does (the camera is off then). On a real screen-off the system may briefly show its wake animation on resume; for completely flicker-free wake use dimming only (turn off = 0). The app window then stays in the foreground and the dashboard is there instantly. - Periodic reload (default 6 h, 0 = off): reloads the dashboard on a timer so the
WebView’s memory (DOM/JS/image cache) does not keep growing over days – the main
defense against Android killing the app for
LOW_MEMORYin long-term operation.
Under Settings → System:
- PIN protection for settings (off by default): when switched off, Settings opens from the menu without a PIN – handy on a tablet nobody else touches.
Under Settings → System → Permissions (grant once; granted permissions show a “✓”):
- Allow camera → for motion detection (front camera).
- Allow microphone → for sound wake.
- Enable device admin → needed for
cmd/lockand “turn screen off”. - Allow brightness control (WRITE_SETTINGS) → real hardware brightness over the full range (fixes “only up to ~60 %”).
- Disable battery optimization → so Android does not kill the app in the background.
- Display over other apps → lets the service bring the dashboard back to the front automatically if Android ever kills the app for memory (see 24/7 operation).
The device-admin button shows the current status (“Device admin active ✓”) and falls back to the security settings if the direct dialog is unavailable on the device.
Android's WebView has no speechSynthesis – Web Speech output simply does not
exist there (verified on Android 15 / WebView 151: webkitSpeechRecognition is
present, speechSynthesis is not). For dashboards that want to speak, FullScreendy
exposes a small bridge to the page:
fullscreendy.textToSpeech("Der Keller ist zu kalt"); // speaks via Android TTS
fullscreendy.stopSpeech(); // stops playback
fullscreendy.getVersion(); // e.g. "0.4.6"textToSpeech honours Settings → Behavior → Text-to-speech enabled.
Microphone for web pages: a page's getUserMedia() request (voice control,
wake-word engines) is denied by default – the Android permission alone is not
enough, the WebView needs its own grant. Under Settings → Behavior → Microphone
you pick what the microphone is used for: Nothing, Wake on sound, or Voice
control in dashboard. It is a single choice on purpose – the microphone can only
serve one consumer at a time, so wake-on-sound and a listening dashboard would block
each other. Only audio is granted, and only while Voice control is selected.
getUserMedia also requires a secure context, so the dashboard must be served over
HTTPS – the settings screen warns if the default dashboard is a plain http://
URL.
A ready-made patch for the FHEM wiki script
VoiceControl: Web-STT
is in tools/voicecontrol-fullscreendy.patch; tools/webcheck.js is a diagnostic
panel (attr WEB JavaScripts www/pgm2/webcheck.js) that reports which speech APIs
the displaying browser actually has.
MainActivity ──── WebView (fullscreen dashboard) + menu/overlay/brightness
▲ KioskBus (commands)
│
KioskService (foreground) ── MqttManager (Paho, auto-reconnect, last will)
├── BatteryMonitor → …/battery, …/charging, …
├── TtsManager ← …/cmd/tts
├── MediaManager ← …/cmd/mediaplay
├── SoundDetector → wakes (AudioRecord, loudness)
└── MotionDetector → …/motion, …/presence (CameraX, light-compensated)
Settings live in Jetpack DataStore; all topics are derived from them.
cmd/unlockonly wakes and dismisses an insecure lock screen; a secured PIN/pattern cannot be bypassed for security reasons.- Motion/sound wake only work while the screen is technically on – hence the
dim overlay instead of a real screen-off. If you let the display go off via the
Android timeout, the camera stops and motion no longer wakes; then only
cmd/screen on(wakelock) helps. - Camera/microphone only start once the app has been in the foreground; after a plain boot without opening it, they stay off (Android background restriction).
- Autostart after boot is unreliable depending on the manufacturer (Android blocks background activity starts) – allow the OEM autostart for the app if needed.
- TLS uses the system trust store; self-signed broker certificates would need to be added.
For continuous operation:
- Settings → System → Permissions → Disable battery optimization – otherwise Android may kill the app after some hours.
- The service uses the specialUse FGS type (Android 14+) – the previous
dataSynctype was hard-stopped by Android 15 after ~6 h per day.
Against LOW_MEMORY kills (the app runs fine in the morning but is gone by
evening): several measures keep the memory footprint low and recover the UI if Android
still reaps the app:
largeHeapfor a bigger heap ceiling, andonTrimMemoryfrees the WebView cache proactively when the system signals memory pressure.- Periodic reload (see Display) resets the WebView memory that grows over days.
- The motion camera analyses at a small 320×240 resolution (smaller frame buffers).
- UI auto-recovery: after a kill,
START_STICKYrestarts only the service, not the dashboard – so the service relaunches the kiosk Activity itself (needs the “Display over other apps” permission). Without it the screen would stay black (“not wakeable”) even though MQTT briefly reconnects.
If the app still “disappears” at some point:
- The MQTT reading
lastExitshows the reason of the last process death after restart (e.g.CRASH,ANR,LOW_MEMORY,USER_REQUESTED,SIGNALED). - A log file with heartbeat (every 30 min, incl. memory:
heap=…/… free=… lowMemory=…), start/stop, cold-restart and crash stack traces:Android/data/de.kewl.fullscreendy/files/logs/app.log(reachable via “Allow file access” + file manager, oradb pull).
Green dot in the status bar: that is Android’s privacy indicator – it shows the camera (motion detection) is active. Apps cannot hide it; it only disappears when motion detection is off.
The app checks GitHub Releases when you open About: stable builds look for the latest stable release, dev builds also consider pre-releases. If a newer version exists you can download and install it right from the About screen – with a progress bar in percent (allow “install unknown apps” once). About also shows app/Android version, IP address and device ID, and links to star the project on GitHub, the source code and support the project.
MIT © Glenn-Dandy. Free to use, modify and distribute (including commercially) as long as the copyright and license notice are retained. No warranty.
Bundled libraries keep their own licenses (e.g. Eclipse Paho, AndroidX/Jetpack Compose – Apache-2.0).