Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@ full story in English and Dutch. Features marked *(app ≥ x.y.z)* need a matchi

## [Unreleased]

## [v1.27.0] — 2026-10-14 (camera popup look, button style, video height)
All contributed by @pelkiegr (ha-pipup #42, #43, #44; app side PiPup #69-#71). *(app ≥ 0.28.0)*; every field
is sent only when set and an older app ignores it.
### Added
- **`live_badge`**, `live_badge_color`, `live_badge_position`, **`media_full_bleed`** and **`buttons_inline`**
in `pipup.show` (#44): the camera popup look.
- **`button_color`**, `button_focus_color`, `button_text_color`, `button_border_color`,
`button_corner_radius` (#43): button style.
- **`media_height` now also applies to video popups** (`camera_entity` stream/mjpeg and `video_url`) as
`media.video.height` (#42). Only a per-call value is sent: the per-TV default (360 on existing setups)
would otherwise change every video popup's poster sizing.

## [v1.26.0] — 2026-10-14 (popup stacking)
### Added
- **`stack: vertical | horizontal | none`** in `pipup.show` *(app ≥ 0.27.0)*: line a popup up with
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,13 @@ Requires the [PiPup fork APK](https://github.com/mhoogenbosch/PiPup/releases) on
edge sit one above the other instead of on top of each other. The rest move up when one closes.
Set a per-TV default under *Configure* (*Default stacking*); `stack: none` overrides it for one
call. An older app ignores the field and the popups overlap as before.
- **Camera popup look and button style** (app >= 0.28.0, contributed by @pelkiegr):
`live_badge` (+ `live_badge_color`, `live_badge_position`) shows a "Live" pill once the stream plays,
`media_full_bleed` runs the media edge to edge, and `buttons_inline` puts the buttons beside the text.
`button_color`, `button_focus_color`, `button_text_color`, `button_border_color` and
`button_corner_radius` restyle the buttons. A per-call `media_height` now also fixes a video popup's
box until the first frame (the per-TV default is deliberately not sent for video). Everything is sent
only when set; an older app ignores it.
- **Push instead of poll** (app >= 0.24.0): at setup every TV gets its own local-only webhook, and
the app POSTs its state there on every change. Popup, screen and overlay entities follow within
milliseconds instead of at the next 15 s poll. A 60 s heartbeat poll remains, because a TV that
Expand Down
11 changes: 11 additions & 0 deletions custom_components/pipup/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,17 @@
ATTR_BRING_TO_FRONT: Final = "bring_to_front"
ATTR_ALL: Final = "all"
ATTR_BUTTON_SIZE: Final = "button_size"
ATTR_BUTTON_COLOR: Final = "button_color"
ATTR_BUTTON_FOCUS_COLOR: Final = "button_focus_color"
ATTR_BUTTON_TEXT_COLOR: Final = "button_text_color"
ATTR_BUTTON_BORDER_COLOR: Final = "button_border_color"
ATTR_BUTTON_CORNER_RADIUS: Final = "button_corner_radius"
ATTR_LIVE_BADGE: Final = "live_badge"
ATTR_LIVE_BADGE_COLOR: Final = "live_badge_color"
ATTR_LIVE_BADGE_POSITION: Final = "live_badge_position"
LIVE_BADGE_POSITIONS: Final = ["top_left", "top_right", "bottom_left", "bottom_right"]
ATTR_MEDIA_FULL_BLEED: Final = "media_full_bleed"
ATTR_BUTTONS_INLINE: Final = "buttons_inline"
ATTR_ANIMATION: Final = "animation"
# app >= 0.27.0: line up with other popups at the same position (#64)
ATTR_STACK: Final = "stack"
Expand Down
2 changes: 1 addition & 1 deletion custom_components/pipup/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"iot_class": "local_polling",
"issue_tracker": "https://github.com/mhoogenbosch/ha-pipup/issues",
"requirements": [],
"version": "1.26.0",
"version": "1.27.0",
"zeroconf": [
"_pipup._tcp.local."
]
Expand Down
57 changes: 57 additions & 0 deletions custom_components/pipup/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,22 @@
ATTR_ANIMATION,
ATTR_STACK,
ATTR_BUTTON_SIZE,
ATTR_BUTTON_BORDER_COLOR,
ATTR_BUTTON_COLOR,
ATTR_BUTTON_CORNER_RADIUS,
ATTR_BUTTON_FOCUS_COLOR,
ATTR_BUTTON_TEXT_COLOR,
ATTR_LIVE_BADGE,
ATTR_LIVE_BADGE_COLOR,
ATTR_LIVE_BADGE_POSITION,
LIVE_BADGE_POSITIONS,
ATTR_DISMISS_SCREENSAVER,
ATTR_ICON,
ATTR_ICON_POSITION,
ATTR_ICON_WIDTH,
ATTR_IMAGE_URL,
ATTR_BUTTONS_INLINE,
ATTR_MEDIA_FULL_BLEED,
ATTR_MEDIA_HEIGHT,
ATTR_MEDIA_WIDTH,
ATTR_MESSAGE,
Expand Down Expand Up @@ -146,11 +157,23 @@
vol.Coerce(int), vol.Range(min=1, max=2160)
),
vol.Optional(ATTR_MUTED): cv.boolean,
vol.Optional(ATTR_MEDIA_FULL_BLEED): cv.boolean,
vol.Optional(ATTR_BUTTONS_INLINE): cv.boolean,
vol.Optional(ATTR_BUTTON_SIZE): vol.All(vol.Coerce(float), vol.Range(min=4, max=96)),
vol.Optional(ATTR_BUTTON_COLOR): vol.Match(COLOR_REGEX),
vol.Optional(ATTR_BUTTON_FOCUS_COLOR): vol.Match(COLOR_REGEX),
vol.Optional(ATTR_BUTTON_TEXT_COLOR): vol.Match(COLOR_REGEX),
vol.Optional(ATTR_BUTTON_BORDER_COLOR): vol.Match(COLOR_REGEX),
vol.Optional(ATTR_BUTTON_CORNER_RADIUS): vol.All(
vol.Coerce(float), vol.Range(min=0, max=128)
),
vol.Optional(ATTR_PADDING): vol.All(vol.Coerce(int), vol.Range(min=0, max=256)),
vol.Optional(ATTR_OPACITY): vol.All(vol.Coerce(float), vol.Range(min=0, max=1)),
vol.Optional(ATTR_TRANSPARENT): cv.boolean,
vol.Optional(ATTR_BRING_TO_FRONT): cv.boolean,
vol.Optional(ATTR_LIVE_BADGE): cv.boolean,
vol.Optional(ATTR_LIVE_BADGE_COLOR): vol.Match(COLOR_REGEX),
vol.Optional(ATTR_LIVE_BADGE_POSITION): vol.In(LIVE_BADGE_POSITIONS),
vol.Optional(ATTR_ANIMATION): vol.In(ANIMATIONS),
vol.Optional(ATTR_STACK): vol.In(STACKS),
vol.Optional(ATTR_SOUND): cv.string,
Expand Down Expand Up @@ -327,6 +350,17 @@ def pick_numeric(attr: str, conf: str) -> Any:
# per-device default. "none" suppresses a default animation for one call.
if (button_size := pick(ATTR_BUTTON_SIZE, CONF_DEFAULT_BUTTON_SIZE, None)):
payload["buttonSize"] = button_size
# app >= 0.28.0 (older ones ignore it): button style; each part is sent only when set
for attr, key in (
(ATTR_BUTTON_COLOR, "buttonColor"),
(ATTR_BUTTON_FOCUS_COLOR, "buttonFocusColor"),
(ATTR_BUTTON_TEXT_COLOR, "buttonTextColor"),
(ATTR_BUTTON_BORDER_COLOR, "buttonBorderColor"),
):
if color := data.get(attr):
payload[key] = color
if (radius := data.get(ATTR_BUTTON_CORNER_RADIUS)) is not None:
payload["buttonCornerRadius"] = radius
if (padding := data.get(ATTR_PADDING)) is not None:
payload["padding"] = padding
# app >= 0.24.0: whole-popup alpha; an older app ignores it
Expand All @@ -335,6 +369,19 @@ def pick_numeric(attr: str, conf: str) -> Any:
# app >= 0.24.0: a redraw opens on top of the other popups
if data.get(ATTR_BRING_TO_FRONT):
payload["bringToFront"] = True
# app >= 0.28.0 (older ones ignore it): a LIVE pill over the stream once it plays
if data.get(ATTR_LIVE_BADGE):
payload["liveBadge"] = True
if color := data.get(ATTR_LIVE_BADGE_COLOR):
payload["liveBadgeColor"] = color
if position := data.get(ATTR_LIVE_BADGE_POSITION):
payload["liveBadgePosition"] = position
# app >= 0.28.0 (older ones ignore it): media edge to edge, text and buttons padded
if data.get(ATTR_MEDIA_FULL_BLEED):
payload["mediaFullBleed"] = True
# app >= 0.28.0: buttons at the end of the title/message row instead of below it
if data.get(ATTR_BUTTONS_INLINE):
payload["buttonsInline"] = True
animation = data.get(ATTR_ANIMATION)
if animation is None:
animation = opts.get(CONF_DEFAULT_ANIMATION)
Expand Down Expand Up @@ -419,8 +466,16 @@ def pick_numeric(attr: str, conf: str) -> Any:
height = pick(ATTR_MEDIA_HEIGHT, CONF_DEFAULT_MEDIA_HEIGHT, 360)
muted = data.get(ATTR_MUTED, opts.get(CONF_DEFAULT_MUTED, True))

# app >= 0.28.0 (older ones ignore it): an explicit per-call media_height fixes a video popup's box until the
# stream reports its display aspect, so an anamorphic camera snapshot (an NVR's 704x480
# still of a 4:3 picture) shows at the right shape. Only a per-call value is sent: the
# per-device default would change every existing video popup's poster sizing.
video_height = data.get(ATTR_MEDIA_HEIGHT)

if stream_uri:
payload["media"] = {"video": {"uri": stream_uri, "width": width, "muted": muted}}
if video_height:
payload["media"]["video"]["height"] = video_height
elif web_uri:
payload["media"] = {
"web": {"uri": web_uri, "width": width, "height": height, "muted": muted}
Expand All @@ -443,6 +498,8 @@ def pick_numeric(attr: str, conf: str) -> Any:
payload["media"]["whep"]["transparent"] = True
elif url := data.get(ATTR_VIDEO_URL):
payload["media"] = {"video": {"uri": url, "width": width, "muted": muted}}
if video_height:
payload["media"]["video"]["height"] = video_height
elif url := data.get(ATTR_IMAGE_URL):
payload["media"] = {"image": {"uri": url, "width": width}}

Expand Down
44 changes: 44 additions & 0 deletions custom_components/pipup/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ show:
muted:
selector:
boolean:
media_full_bleed:
selector:
boolean:
buttons_inline:
selector:
boolean:
padding:
selector:
number:
Expand All @@ -60,13 +66,51 @@ show:
bring_to_front:
selector:
boolean:
live_badge:
selector:
boolean:
live_badge_color:
example: "#FFA500"
selector:
text:
live_badge_position:
selector:
select:
options:
- top_left
- top_right
- bottom_left
- bottom_right
button_size:
selector:
number:
min: 4
max: 96
unit_of_measurement: sp
mode: box
button_color:
example: "#40FFFFFF"
selector:
text:
button_focus_color:
example: "#66FFFFFF"
selector:
text:
button_text_color:
example: "#FFFFFF"
selector:
text:
button_border_color:
example: "#4DFFFFFF"
selector:
text:
button_corner_radius:
selector:
number:
min: 0
max: 128
unit_of_measurement: px
mode: box
animation:
selector:
select:
Expand Down
42 changes: 41 additions & 1 deletion custom_components/pipup/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@
},
"media_height": {
"name": "Media height",
"description": "Web view height in pixels (default 360)."
"description": "Media height in pixels: the web/WHEP view height (default 360), and for a camera or video popup the box height until the stream reports its own shape, so an anamorphic camera snapshot shows correctly; older apps ignore it."
},
"title_color": {
"name": "Title color",
Expand All @@ -385,6 +385,14 @@
"name": "Mute audio",
"description": "Mutes audio in video/web streams. When omitted, the device default applies (integration settings; audio can freeze video on some TVs)."
},
"media_full_bleed": {
"name": "Full-bleed media",
"description": "Run the video or image edge to edge across the top of the popup, its top corners rounded with the popup; title, message and buttons keep the padding. Older apps ignore it."
},
"buttons_inline": {
"name": "Inline buttons",
"description": "Put the buttons at the end of the title/message row instead of on their own row below it, e.g. a single compact ✕. Older apps ignore it."
},
"tts": {
"name": "Speak text",
"description": "Text spoken aloud on the TV when the popup appears (app 0.2.5 or newer)."
Expand Down Expand Up @@ -449,6 +457,26 @@
"name": "Button size",
"description": "Button text size in sp; padding scales along, so popups with buttons can be small (app 0.19.0 or newer). Omit for the classic look."
},
"button_color": {
"name": "Button color",
"description": "Fill of an unfocused button as #RRGGBB or #AARRGGBB; older apps ignore it. Omit for the classic look."
},
"button_focus_color": {
"name": "Button focus color",
"description": "Fill of the focused button as #RRGGBB or #AARRGGBB; older apps ignore it. Omit for the classic blue."
},
"button_text_color": {
"name": "Button text color",
"description": "Button label color as #RRGGBB or #AARRGGBB; older apps ignore it. Omit for white."
},
"button_border_color": {
"name": "Button border color",
"description": "Button outline color, focused and unfocused, as #RRGGBB or #AARRGGBB; older apps ignore it."
},
"button_corner_radius": {
"name": "Button corner radius",
"description": "Button corner radius in pixels; 0 gives square corners, a large value a pill. Older apps ignore it."
},
"animation": {
"name": "Animation",
"description": "Entrance animation, reversed when the popup expires naturally. An update-in-place of the same popup does not re-animate (app 0.19.0 or newer)."
Expand All @@ -472,6 +500,18 @@
"bring_to_front": {
"name": "Redraw on top",
"description": "Only when a popup with this ID is already on the TV: draw the new one on top of the other popups instead of in its old place (app 0.24.0 or newer)."
},
"live_badge": {
"name": "Live badge",
"description": "Show a Live pill over a camera or video popup once the stream is playing (never over the still); older apps ignore it."
},
"live_badge_color": {
"name": "Live badge color",
"description": "Live pill color as #RRGGBB or #AARRGGBB; default amber."
},
"live_badge_position": {
"name": "Live badge position",
"description": "Corner of the video for the Live pill; default bottom right."
}
}
},
Expand Down
42 changes: 41 additions & 1 deletion custom_components/pipup/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@
},
"media_height": {
"name": "Media height",
"description": "Web view height in pixels (default 360)."
"description": "Media height in pixels: the web/WHEP view height (default 360), and for a camera or video popup the box height until the stream reports its own shape, so an anamorphic camera snapshot shows correctly; older apps ignore it."
},
"title_color": {
"name": "Title color",
Expand All @@ -385,6 +385,14 @@
"name": "Mute audio",
"description": "Mutes audio in video/web streams. When omitted, the device default applies (integration settings; audio can freeze video on some TVs)."
},
"media_full_bleed": {
"name": "Full-bleed media",
"description": "Run the video or image edge to edge across the top of the popup, its top corners rounded with the popup; title, message and buttons keep the padding. Older apps ignore it."
},
"buttons_inline": {
"name": "Inline buttons",
"description": "Put the buttons at the end of the title/message row instead of on their own row below it, e.g. a single compact ✕. Older apps ignore it."
},
"tts": {
"name": "Speak text",
"description": "Text spoken aloud on the TV when the popup appears (app 0.2.5 or newer)."
Expand Down Expand Up @@ -449,6 +457,26 @@
"name": "Button size",
"description": "Button text size in sp; padding scales along, so popups with buttons can be small (app 0.19.0 or newer). Omit for the classic look."
},
"button_color": {
"name": "Button color",
"description": "Fill of an unfocused button as #RRGGBB or #AARRGGBB; older apps ignore it. Omit for the classic look."
},
"button_focus_color": {
"name": "Button focus color",
"description": "Fill of the focused button as #RRGGBB or #AARRGGBB; older apps ignore it. Omit for the classic blue."
},
"button_text_color": {
"name": "Button text color",
"description": "Button label color as #RRGGBB or #AARRGGBB; older apps ignore it. Omit for white."
},
"button_border_color": {
"name": "Button border color",
"description": "Button outline color, focused and unfocused, as #RRGGBB or #AARRGGBB; older apps ignore it."
},
"button_corner_radius": {
"name": "Button corner radius",
"description": "Button corner radius in pixels; 0 gives square corners, a large value a pill. Older apps ignore it."
},
"animation": {
"name": "Animation",
"description": "Entrance animation, reversed when the popup expires naturally. An update-in-place of the same popup does not re-animate (app 0.19.0 or newer)."
Expand All @@ -472,6 +500,18 @@
"bring_to_front": {
"name": "Redraw on top",
"description": "Only when a popup with this ID is already on the TV: draw the new one on top of the other popups instead of in its old place (app 0.24.0 or newer)."
},
"live_badge": {
"name": "Live badge",
"description": "Show a Live pill over a camera or video popup once the stream is playing (never over the still); older apps ignore it."
},
"live_badge_color": {
"name": "Live badge color",
"description": "Live pill color as #RRGGBB or #AARRGGBB; default amber."
},
"live_badge_position": {
"name": "Live badge position",
"description": "Corner of the video for the Live pill; default bottom right."
}
}
},
Expand Down
Loading
Loading