Skip to content

Sending notifications

A notification on the front panel is not "some text" — it is a handful of elements placed to the pixel on 72 × 16, and where they go depends on the font, on whether an icon takes the left edge, and on what the firmware in front of you can draw.

The library places them, so you describe the notification and not its geometry:

from busylib import AsyncBusyBar
from busylib.features import notify

bar = AsyncBusyBar("10.0.4.20")
await bar.version()  # so the version-dependent parts can be checked
await notify(bar, "Laundry done", icon="check", duration=10)

notify() builds the elements and draws them. To build without drawing — to inspect the payload, or to send it yourself — use build_notification(), which returns a DisplayElements model.

What you can set

Field Meaning
line_1 The first (or only) line
line_2 A second line; giving it selects the two-line layout
icon A name from STOCK_ICONS, drawn at the left edge
line_1_color, line_2_color Per-line colour
background_color Fills the panel behind everything
line_1_font The first line's size, and the second's unless it has its own
line_2_font A size for the second line alone
duration Seconds before the elements expire
priority PRIORITY_DEFAULT, or PRIORITY_INTERRUPT to sit above a Busy session
sound A name from STOCK_SOUNDS, played with the drawing
application_name Owns the drawing, and is how display_clear finds it

Two sizes say which line matters. A label over a detail - line_1_font="bold", line_2_font="tiny" - reads at a glance from across a room, where one size for both reads as two equal things:

await notification.notify(
    bar,
    "MEETING",
    line_2="until 15:30",
    line_1_font="bold",
    line_2_font="tiny",
    application_name="my-app",
)

Each line is anchored to its own edge of the panel, so each takes the offset its own font needs. Two lines only fit the five shorter fonts, and that limit applies to each line separately - large on either of them is refused rather than drawn off the panel.

A drawing loses to anything above it, and the device answers 409 Not drawn due to low priority rather than drawing it late.

Built-in templates

There are two, and the one that suits the arguments is chosen for you:

Template Chosen when Fonts
ONE_LINE no line_2 all seven
TWO_LINES line_2 given tiny, small, normal, condensed, bold

Two templates cover four layouts: an icon only changes where the text starts, which is worked out from the icon's own width, so "with an icon" is not a separate arrangement. The shipped icons are 5, 8 and 11 pixels wide, which is why that offset is computed rather than fixed.

large and extra_large are missing from the two-line row because two lines of them do not fit 16 pixels. Asking anyway is an error rather than a silent substitution — a caller who asked for large and got small has no way to notice.

Writing your own

A template is anything with name, fonts, matches() and render(). The built-ins are ordinary implementations of that protocol, so writing one is the same work:

from busylib.features import notification


class RightAligned:
    """One line, pushed against the right edge."""

    name = "right_aligned"
    fonts = ("small", "bold")

    def matches(self, spec: notification.NotificationSpec) -> bool:
        return not spec.line_2

    def render(self, spec: notification.NotificationSpec):
        right = spec.display.width - 2
        return [
            spec.text(
                "1",
                spec.line_1,
                y=8,
                align="mid_right",
                x=right,
                width=right,
            )
        ]


await notify(bar, "CUSTOM", template=RightAligned())

render() receives a NotificationSpec with the work you should not repeat already done:

On the spec What it gives you
icon The icon resolved to its path and width, or None
text_x Where a left-anchored line starts, past the icon
available_width What is left for text after the icon
display The panel's width, height and name
text(...) A text element, scrolled if it cannot fit
icon_element() The icon element, or None

matches() is only consulted for the built-ins, when no template was passed. A template you pass is used regardless, so matches() can simply return True if you never register it for selection.

Two defaults that assume a left anchor

spec.text() defaults x to spec.text_x and the scroll window to everything right of x. Both are right for a line anchored left, and wrong for any other anchor:

  • with align="mid_right" and the default x, the line's right edge lands at the left margin and the text runs off the panel;
  • with a right-hand x and the default width, the room is computed as the few pixels right of it, and the line scrolls through a window that narrow.

Pass both x and width for any anchor other than the left one, as the example above does.

Backgrounds need API 24.3.0

A background colour is drawn as a filled rectangle, which firmware below 24.3.0 has no primitive for. Asking for one there raises rather than drawing something else:

from busylib.exceptions import BusyBarFeatureUnavailableError

try:
    await notify(bar, "hi", background_color=[0, 0, 90])
except BusyBarFeatureUnavailableError as err:
    print(
        err.feature,
        "needs",
        err.required_version,
        "but the bar reports",
        err.device_version,
    )

The check is why notify() takes the client rather than a version: it reads bar.device_api_version, which version() fills in. If nothing has asked the bar yet, the version is unknown and a current device is assumed — guessing "old" would disable the feature for everyone who never called version().

This is also the part a custom template never has to think about. The background is added by build_notification(), not by the template, so a template you write cannot forget the check.

Icons and sounds

STOCK_ICONS and STOCK_SOUNDS name what ships on the device:

from busylib.features import notification

print(sorted(notification.STOCK_ICONS))
print(sorted(notification.STOCK_SOUNDS))

Expected output:

['check', 'clock', 'error', 'hourglass', 'info', 'low_battery', 'setup', 'start']
['event', 'reminder', 'volume']

Pass a sound name to notify() rather than playing it yourself - it is a second request, and it has to carry the same application_name as the drawing or the two belong to different owners on the device:

await notify(bar, "Laundry done", icon="check", sound="event")

Paths in both catalogues carry their sub-folder and extension. The flat shared/<name> form that the OpenAPI spec suggests is refused.

Those eight are a short-hand, not the limit. Icons are files on the bar - the firmware ships a set, the Draw Tool adds its own, and an owner can upload or delete them - so notify() takes any name the bar has:

print(
    await notification.icons(bar)
)  # {'dt_burger': 'shared/images/dt_burger.image', ...}

await notify(bar, "Lunch", icon="dt_burger")

The width matters and is read from the file rather than assumed. The Draw Tool's icons are 16 wide where the built-in ones are 5, 8 or 11, and only some file names say so - guessing is how an icon and its text end up on top of each other. A 16-wide icon fills the panel's height and leaves 56 pixels for text, so a two-line layout with one is tight by design.

Withdrawing a notification

There is no clearing helper, because there is nothing to compose - a drawing is withdrawn by its owner:

await bar.display_clear(application_name="home_assistant")

Most notifications do not need it: duration expires the elements on their own, and a short one is the usual way to replace a notification with nothing.