Generated by aurora@aurora-smart-home (ha-integration-dev skill)
Control PiPup popups on Android TV / Fire TV devices from Home Assistant — including showing a camera stream for as long as there is motion.
Requires the PiPup fork APK on the TV
(the original Play Store version lacks the /state endpoint and indefinite popups).
- Config flow per TV (host + port) with automatic mDNS discovery (app ≥ 0.2.5 advertises
_pipup._tcp); discovered TVs carry a stable device id, so the config entry follows the TV across DHCP address changes (existing host:port entries migrate automatically on first contact). Each TV becomes a device with:- Popup binary sensor (on = a popup is visible; attributes:
popup_id,duration,indefinite,elapsed) - Screen binary sensor (TV screen on/interactive — requires app ≥ 0.2.3)
- Connectivity binary sensor — FireTV sticks cut their network in standby; entities now stay available with their last state (popup/screen report unknown) and this sensor tells you whether the TV currently answers, so automations can trigger on its off→on edge
- Notification notify entity — the TV works as a standard
notify.send_messagetarget (text-only popups honoring the per-device defaults) - Current popup sensor (the visible popup id) and Popups shown counter
- Default position select — the position used when
pipup.showis called without one - Dismiss popup button
- PiPup app update entity — checks the fork's GitHub releases and, with app >= 0.6.0, the Install button makes the TV update itself (silent on Android 12+, system confirmation on older devices)
- App uptime diagnostic sensor and a diagnostics download
- Popup binary sensor (on = a popup is visible; attributes:
- Action
pipup.show— title/message/media popup with all PiPup fields, plus:duration: 0→ popup stays until dismissed or replacedpopup_id→ re-sending the same id+content only extends the timer (stream keeps playing, no flicker)camera_entity→ shows the camera without any URLs;camera_modepicks mjpeg (default — software-decoded, audio-free, safe while the TV is playing video), stream (HLS, hardware decoder — can freeze concurrent live-TV playback on some devices) or snapshotpositionis optional — omitted means the device's configured default positionmuted: trueby default (requires app ≥ 0.2.4) — audio in a popup can freeze video playback on some TVs; passmuted: falsewhen you do want sound- per-device defaults for everything — duration, position, muted, media size, colors and text sizes are configurable per TV in the integration options (Settings → Devices & Services → PiPup → Configure); action fields act as one-off overrides
tts→ text spoken aloud on the TV when the popup appears (app ≥ 0.2.5); optionaltts_languagetakes a BCP-47 tag such asnl-NL(device default when omitted)buttons→ up to 3 remote-operable buttons on the popup (app ≥ 0.3.0): OK activates, BACK dismisses. A press fires thepipup_buttonevent (withpopup_id,button,label,device_id) via an automatically registered local-only webhook — no configuration needed; see the doorbell example belowshow_progress→ animated countdown bar for finite durations (app ≥ 0.3.0)urgency→info/warning/criticalcolored border presets (app ≥ 0.3.0)
- Action
pipup.dismiss— remove the popup, optionally only when it has a givenpopup_id.
- HACS → Integrations → Custom repositories
- Add
https://github.com/mhoogenbosch/ha-pipupas Integration - Install "PiPup" and restart Home Assistant
- Copy
custom_components/pipupto yourcustom_components/ - Restart Home Assistant
TVs running the fork app ≥ 0.2.5 are discovered automatically — they show up under Settings → Devices & Services as a discovered PiPup device; press Configure, optionally set a name and a name suffix, done. Because entries key on the app's stable device id, they keep working when the TV gets a different DHCP address.
Manual fallback (older app, or discovery blocked between VLANs): Settings → Devices & Services → Add Integration → PiPup → enter the TV's IP and port (default 7979).
Per-TV popup defaults (position, duration, muted, sizes, colors) live under Configure on each entry; action fields override them per call.
Real-world recipes from the household this integration was built for. All popup styling (position, colors, size) falls back to the per-device defaults, so the calls stay short.
One script pops a message on every TV — straight through games and headphone sessions — and speaks it out loud (app ≥ 0.2.5). Unreachable/sleeping TVs are skipped silently.
script:
announce:
alias: House announcement
fields:
message: {required: true}
sequence:
- action: pipup.show
continue_on_error: true # sleeping TVs must not abort the rest
target:
entity_id:
- binary_sensor.pipup_living_room_popup
- binary_sensor.pipup_kids_room_popup
- binary_sensor.pipup_bedroom_popup
data:
popup_id: announce
title: "📢 Announcement"
message: "{{ message }}"
duration: 15
tts: "{{ message }}"
tts_language: nl-NL
muted: falseWire it to a dashboard button with tap_action → perform-action: script.announce,
data: {message: "Dinner is ready!"} — add a confirmation: so nobody summons the
whole house by accident.
The message field takes Jinja, so a "mini dashboard" is just a multi-line template.
With show_progress: true (app ≥ 0.3.0) the popup shows a countdown bar.
action: pipup.show
target:
entity_id: binary_sensor.pipup_living_room_popup
data:
popup_id: energy
duration: 25
position: center
title: "⚡ Energy & car"
show_progress: true
message: >-
{% set net = states('sensor.grid_power') | int(0) %}
☀️ Solar: {{ states('sensor.pv_power') | int(0) }} W
🏠 House: {{ states('sensor.house_power') | int(0) }} W
🔌 Grid: {{ net | abs }} W {{ 'import' if net > 0 else 'export' }}
🚗 Battery: {{ states('sensor.car_battery') | int(0) }}%Buttons (app ≥ 0.3.0) are operated with the TV remote: OK activates, BACK dismisses.
A press fires the pipup_button event, so the acting automation can (and should) filter on
both the button id and the device id — only the intended TV can open the door.
# 1) the doorbell automation shows the popup with a button
- action: pipup.show
target:
entity_id: binary_sensor.pipup_living_room_popup
data:
popup_id: doorbell
duration: 45 # finite: never leave an undismissable popup
title: Front door
tts: "Someone is at the door"
web_url: "http://go2rtc.local:1984/stream.html?src=doorbell&mode=webrtc"
media_width: 720
media_height: 540
buttons:
- id: unlock_front_door
label: "🔓 Open the door"
# 2) a second automation acts on the press
triggers:
- trigger: event
event_type: pipup_button
event_data:
button: unlock_front_door
device_id: "1d91b16a-…" # the app's stable id — check the Diagnostics download
actions:
- action: lock.open
target:
entity_id: lock.front_door
- action: notify.mobile_app_phone # audit trail
data:
message: "Front door opened via the living-room TV"triggers:
- trigger: calendar
entity_id: calendar.family
event: start
offset: "-00:15:00"
conditions:
- "{{ not trigger.calendar_event.all_day }}" # all-day events would pop at 23:45
actions:
- action: pipup.show
target:
entity_id: binary_sensor.pipup_living_room_popup
data:
popup_id: agenda
duration: 30
show_progress: true
title: "📅 Up next ({{ trigger.calendar_event.start | as_timestamp | timestamp_custom('%H:%M') }})"
message: "{{ trigger.calendar_event.summary }}"triggers:
- trigger: state
entity_id: binary_sensor.driveway_motion
to: "on"
id: motion_on
- trigger: state
entity_id: binary_sensor.driveway_motion
to: "off"
for: "00:00:15"
id: motion_off
actions:
- choose:
- conditions: "{{ trigger.id == 'motion_on' }}"
sequence:
- action: pipup.show
target:
entity_id: binary_sensor.pipup_living_room_popup
data:
title: Motion on the driveway
duration: 0 # until dismissed
popup_id: driveway
camera_entity: camera.driveway # camera_mode defaults to mjpeg (safe next to live TV)
- conditions: "{{ trigger.id == 'motion_off' }}"
sequence:
- action: pipup.dismiss
target:
entity_id: binary_sensor.pipup_living_room_popup
data:
popup_id: driveway # never cancels an unrelated popupWhen you show indefinite (duration: 0) popups, make the automation mode: queued and put
continue_on_error: true on the pipup actions — with the default mode: single a dismiss that
fires while a show is still running is silently dropped, leaving the popup on screen.
On TCL Google TV sets, PiPup does not stay up by itself. TCL runs a vendor guard
(com.tcl.guard) that both kills and freezes background apps, so sooner or later the TV
ends up in a state where the process is still listed but the HTTP server no longer answers: this
integration then reports the device as unreachable and every pipup.show fails, even though the TV
is online and ADB works. See the
app README
for the mechanism, the required app-ops and how to recognise a frozen process.
What matters here: the app survives only when it runs as a foreground service at
oom_score_adj 200, and that happens only if it was started from a foreground context. So the
recovery command must start the activity, not the service — am start-foreground-service lands
on adj 500 and gets frozen again within seconds.
This automation restarts PiPup whenever the integration reports the TV as unreachable while ADB is still answering. It needs the Android TV / ADB integration for the same TV.
alias: PiPup keep-alive (TCL)
description: >
Restarts PiPup on a TCL Google TV after the vendor guard killed or froze it.
Starts the activity (not the service): only a foreground start survives.
mode: single
triggers:
- trigger: state
entity_id: binary_sensor.pipup_living_room_reachable
to: "off"
for: "00:10:00"
- trigger: time_pattern
minutes: /15 # retry while PiPup stays down
- trigger: time
at: "05:00:00" # daily check
conditions:
# only when the TV itself is reachable — otherwise there is nothing to fix
- condition: state
entity_id: media_player.android_tv_living_room
state: ["idle", "standby", "off", "paused"]
- condition: or
conditions:
- condition: state
entity_id: binary_sensor.pipup_living_room_reachable
state: "off"
- condition: state
entity_id: binary_sensor.pipup_living_room_reachable
state: "unavailable"
actions:
- action: androidtv.adb_command
continue_on_error: true
target:
entity_id: media_player.android_tv_living_room
data:
command: >-
input keyevent KEYCODE_WAKEUP; sleep 2;
am start -n nl.rogro82.pipup/.MainActivity; sleep 3;
input keyevent KEYCODE_HOMENotes from running this in practice:
- Do not gate the command on
ps | grep pipup. A frozen process is still listed, so the guard short-circuits exactly when you need the restart. Starting unconditionally is safe. - The
statecondition keeps the automation from interrupting viewing: bringing the activity to the front briefly takes over the screen, so skip it while the TV isplaying. KEYCODE_WAKEUPis needed becauseam startdoes not reach the foreground while the TV is dreaming (screensaver). Note this wakes the screen — that is the price of the recovery.- After the app is reachable again, an integration entry that failed to set up earlier stays in
setup_retry; reload it (Settings → Devices & Services → PiPup → ⋮ → Reload) or the entities remain unavailable. Enabling/disabling the entry does not help — it is already enabled. - If you monitor port 7979 externally (e.g. Uptime Kuma), that sensor is a good extra trigger, because it detects a dead server slightly sooner than the coordinator does.
PiPup runs an unauthenticated HTTP server on each TV, so any device on the network can show
popups (including arbitrary JavaScript via a web popup). Keep PiPup TVs on a trusted network
segment. See the app's Security section.
Button presses arrive over a local-only webhook. To stop a device on the LAN from forging a
pipup_button event (which could drive a security-sensitive automation such as a door lock), this
integration mints a single-use token for every button popup, passes it in the callback URL, and
rejects any callback whose token is missing, unknown or expired. Traffic to the TV is plain HTTP, so
the token can still be sniffed within its short window on an untrusted network — another reason to
keep these devices on a segment you control.
- "unsupported_version" while adding — the TV runs the original PiPup; sideload the fork APK first.
- Connectivity sensor off / popup & screen "unknown" — the TV is off/asleep (FireTV sticks cut their network entirely in standby) or the PiPup service is not running (open the app once after boot). Entities keep their last state on purpose; automate on the connectivity sensor's off→on edge.
- Buttons don't react to the remote — buttons require app ≥ 0.3.0; also note the popup takes input focus while buttons are visible (BACK gives control back to the TV app).
- Camera stream does not play — the TV must be able to reach your Home Assistant internal URL;
check Settings → System → Network → Home Assistant URL. Snapshot mode (
camera_mode: snapshot) works without any URL reachability. - Popup does not appear at all — grant the overlay permission once:
adb shell appops set nl.rogro82.pipup SYSTEM_ALERT_WINDOW allow. - TV is online and ADB works, but PiPup is unreachable (TCL sets) — the vendor guard froze the
process: it is still listed by
pswhile port 7979 no longer answers. Restart PiPup by starting its activity, and automate it — see Device notes: TCL Google TV. - Every
pipup.showfails with "No loaded PiPup devices in the action target" — either the target is not a PiPup entity/device, or the config entry is not loaded (it stays insetup_retrywhen the TV was unreachable at startup). Reload the entry; the target itself may be perfectly fine. Any entity of the device works as a target, not just the popup sensor.
Remove the device via Settings → Devices & Services; re-adding it restores everything (no state is stored on the TV).