feat(plugins): request_on_demand() / end_on_demand() -- plugins ask for the screen in-process (#768)

* feat(plugins): request_on_demand() / end_on_demand() -- plugins ask for the screen in-process

Four plugins (birdnet-go, mqtt-notifications, on-air, pomodoro-timer) take
the screen by writing the display_on_demand_request mailbox, which the
display reads once a second while the control socket is up and which stage 5
removes. This is the in-process way in that stage needed.

- BasePlugin.request_on_demand(mode=None, duration=None, pinned=False) and
  end_on_demand(), safe from any thread, go through PluginManager to
  DisplayController.submit_plugin_on_demand, which only queues (at most 32)
  and wakes the render thread through ControlServer.wake(). The render
  thread applies them in _drain_control_commands, after socket commands,
  through _handle_on_demand_request, so they land within a frame; without a
  socket, on the next pending-changes pass.
- A plugin's stop ends only its own session; a mailbox stop still ends any.
- Both answer the request id, or None with no display in the process (web
  interface, check_plugin.py), a full queue, or a mock manager -- a plugin's
  cue to write the mailbox, which the display still reads.
- docs/PLUGIN_API_REFERENCE.md documents the hasattr pattern for plugins
  that must keep working on older cores; IPC_CONTROL_SOCKET.md and the
  CHANGELOG are updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(display): wire the on-demand handler only on a manager that has it

Tests and the golden traces stand in simpler plugin managers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-10-05 01:39:02 -04:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 5a7893b11a
commit 0577c807eb
8 changed files with 799 additions and 21 deletions
+24
View File
@@ -19,6 +19,30 @@ accepts both, but the store flags the old spelling as deprecated
## Unreleased
### Plugins ask for the screen in-process: `request_on_demand()` / `end_on_demand()`
The in-process way in that stage 5 of the control socket needed
(`docs/IPC_CONTROL_SOCKET.md`, "Plugins in the display process").
- **`BasePlugin.request_on_demand(mode=None, duration=None, pinned=False)`**
shows the plugin now, and **`BasePlugin.end_on_demand()`** gives the
screen back. Both are safe from any thread (an MQTT callback, a timer
thread): `PluginManager.request_on_demand()` / `end_on_demand()` hand the
request to `DisplayController.submit_plugin_on_demand()`, which only
queues it (at most 32) and wakes the render thread through the control
socket's flag (`ControlServer.wake()`). The render thread applies it with
the socket's commands, through the same handler as a web on-demand
request, so it lands within a frame rather than on the mailbox's
once-a-second look. Both return the request id, or `None` when no display
runs in the process (the web interface, `scripts/check_plugin.py`) or the
queue is full.
- **A plugin's stop ends only its own session.** A mailbox stop still ends
any session, whoever started it.
- **Older cores.** Plugins detect the methods with `hasattr` and write the
`display_on_demand_request` mailbox when they are missing or answer
`None`; the pattern is in `docs/PLUGIN_API_REFERENCE.md` ("On-demand
display"). The display still reads the mailbox for plugins that write it.
### Web UI: Schedule and General are ES-module pages (stage 3)
- The Schedule and General tabs follow stage 2 (#727): their inline