So far so good, I contacted the developer of the pipup fork (GitHub - steffjenl/PiPup at 0.1.7) and for now, feed display is not implemented. But it's on his to-do list. I will improve my driver when this feature becomes available. In the meantime, I've improved the driver to support another feature that pipup supports, namely displaying a web page. This is useful for displaying custom content or a link that sends to a web video stream such as mjpeg with Frigate, for example.
Here is the the complete instructions:
-
download latest .APK at: Release 0.1.7 ยท steffjenl/PiPup ยท GitHub
-
download adb (Android debug bridge)
-
use the following command: adb connect IP_address_of_AndroidTV
-
on AndroidTV device, autorize connection
-
use the following command: adb install C:\pathtoapk\pipup\app-release.apk
-
use the following command: adb shell appops set nl.rogro82.pipup SYSTEM_ALERT_WINDOW allow
Please note that you also need to setup a virtual device for your doorbell. See: Reolink Doorbell - receiving Visitor events in Hubitat - #13 by richard-brown
Latest Hubitat driver code:
/**
* PiPup Doorbell Auto v4.2 (Snapshot + Web Media + Fallback)
* Author: Patrick Gagne
* 2025-11-14
*
* New in v4.2:
* - Adds support for media.web (e.g. MJPEG streams or HTTP web pages)
* - Automatically chooses web OR image depending on settings
*/
metadata {
definition (name: 'PiPup Doorbell Auto v4.2', namespace: 'pgagne', author: 'Patrick Gagne') {
capability 'Notification'
capability 'Switch'
command 'notifyDoorbell'
}
preferences {
input(name: "DebugLogging", type: "bool", title:"Enable Debug Logging", defaultValue: true)
input(name: "TVIPAddress", type: "string", title:"Android TV / PiPup IP Address", defaultValue: "")
// Image snapshot
input(name: "SnapshotURL", type: "string", title:"Snapshot URL", defaultValue: "")
input(name: "FallbackImageURL", type: "string", title:"Fallback Image URL",
defaultValue: "https://mir-s3-cdn-cf.behance.net/project_modules/max_1200/cfcc3137009463.5731d08bd66a1.png")
// Web media
input(name: "WebMediaURL", type: "string", title:"Web Media URL (optional)",
description:"If set, the driver sends media.web instead of an image", defaultValue: "")
// Display settings
input(name: "DisplayDuration", type: "number", title:"Duration (seconds)", defaultValue: 15)
input(name: "Position", type: "number", title:"Position (0=TR,1=TL,2=BR,3=BL,4=Center)", defaultValue: 0)
input(name: "DefaultTitle", type: "string", title:"Notification Title", defaultValue: "Doorbell")
input(name: "TitleColor", type: "string", title:"Title Color", defaultValue: "#0066cc")
input(name: "TitleSize", type: "number", title:"Title Size", defaultValue: 20)
input(name: "MessageColor", type: "string", title:"Message Color", defaultValue: "#000000")
input(name: "MessageSize", type: "number", title:"Message Size", defaultValue: 14)
input(name: "BackgroundColor", type: "string", title:"Background Color", defaultValue: "#ffffff")
input(name: "Message", type: "string", title:"Default Message Text", defaultValue: "Someone is at the door!")
}
}
void installed() { debugLog("Installed") }
void updated() { debugLog("Preferences Updated") }
void initialize() { debugLog("Initialized") }
void deviceNotification(String text) {
debugLog("deviceNotification received: ${text}")
notifyDoorbell(text)
}
void on() {
debugLog("Switch ON โ sending notification")
notifyDoorbell()
}
void off() { debugLog("Switch OFF โ no action") }
void notifyDoorbell(String customMessage = null) {
String messageText = customMessage ?: Message
if (!TVIPAddress) {
errorLog("TV IP address is missing!")
return
}
if (WebMediaURL?.trim()) {
sendWebMediaToPiPup(messageText)
} else {
sendSnapshotToPiPup(messageText)
}
}
//
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// IMAGE SNAPSHOT MODE
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
//
private void sendSnapshotToPiPup(String messageText) {
def imageURL = SnapshotURL
if (!imageURL) {
errorLog("Snapshot URL missing โ using fallback")
imageURL = FallbackImageURL
}
// Validate snapshot
try {
httpGet([uri: imageURL, timeout: 5]) { resp ->
if (resp.status != 200) {
debugLog("Snapshot inaccessible (HTTP ${resp.status}) โ fallback")
imageURL = FallbackImageURL
}
}
} catch (Exception e) {
debugLog("Snapshot error: ${e} โ fallback")
imageURL = FallbackImageURL
}
def json = [
duration: DisplayDuration,
position: Position,
title: DefaultTitle,
titleColor: TitleColor,
titleSize: TitleSize,
message: messageText,
messageColor: MessageColor,
messageSize: MessageSize,
backgroundColor: BackgroundColor,
media: [
image: [
uri: imageURL,
width: 480
]
]
]
sendToPiPup(json, "snapshot")
}
//
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// WEB MEDIA MODE (MJPEG, Dashboard, etc.)
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
//
private void sendWebMediaToPiPup(String messageText) {
String url = WebMediaURL
def json = [
duration: DisplayDuration,
position: Position,
title: DefaultTitle,
titleColor: TitleColor,
titleSize: TitleSize,
message: messageText,
messageColor: MessageColor,
messageSize: MessageSize,
backgroundColor: BackgroundColor,
media: [
web: [
uri: url,
width: 640,
height: 480
]
]
]
sendToPiPup(json, "web media")
}
//
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// SHARED PiPUP POST FUNCTION
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
//
private void sendToPiPup(def json, String mode) {
try {
def postParams = [
uri: "http://${TVIPAddress}:7979/notify",
headers: ["Content-Type": "application/json"],
body: groovy.json.JsonOutput.toJson(json)
]
debugLog("JSON (${mode}) sent: ${groovy.json.JsonOutput.toJson(json)}")
httpPost(postParams) { resp ->
debugLog("PiPup response: ${resp.status}")
}
} catch (Exception e) {
errorLog("Error sending notification: ${e}")
}
}
// Logging
def debugLog(msg) { if (DebugLogging) log.debug msg }
def errorLog(msg) { log.error msg }
Device settings example:
