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 defaultx, the line's right edge lands at the left margin and the text runs off the panel; - with a right-hand
xand 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.