Skip to content

Creating Plugins

Developer guide for building OpenAVC plugins. Plugins are Python packages that extend the platform with system-wide integrations, control surfaces, and services.

For installing and using plugins as an end user, see Plugins.

Plugin Extensions

Plugins can contribute content to several places in the Programmer IDE and the touch panel.

Plugin Views

Plugins can add their own views to the sidebar (below the standard sections). These might show a monitoring dashboard, a routing matrix, or a live state table. Plugin views appear automatically when the plugin is running.

Status Cards

Plugins can add status cards to the Dashboard. These show key metrics (connection status, message counts, device counts) as compact cards alongside the standard system status.

Device Panels

Plugins can add panels to the device detail page. These appear below the standard device sections and only show when relevant (e.g., a Dante plugin adds audio channel info to Dante-compatible devices).

Context Actions

Plugins can add action buttons to toolbars. These might appear globally (e.g., “Scan Dante Network”) or on specific device pages (e.g., “Identify on Dante”). Clicking a context action triggers the plugin to perform the action.

Panel Elements

Plugins can provide custom panel elements that appear on the touch panel alongside built-in elements like buttons, labels, and faders. Panel elements are rendered in sandboxed iframes, giving the plugin full control over its visual presentation.

Use cases include video previews (NDI/IP camera), custom routing matrices, live audio meters, floor plan overlays, or any domain-specific widget the built-in elements don’t cover.

Plugin panel elements appear in the Element Palette under a Plugins category. Drag them onto the canvas like any other element. In the Properties panel, you can configure the plugin-specific settings (e.g., source name, display mode) that the plugin author defined.


Panel Element Development

Plugin panel elements use an iframe-based renderer. The plugin provides an HTML page that runs inside a sandboxed iframe on the touch panel. The panel and the iframe communicate through a postMessage API. The panel sends state updates and configuration to the iframe, and the iframe can send commands and state changes back.

This architecture keeps plugins fully isolated from the panel DOM. A plugin cannot break the panel layout or interfere with other elements.

Defining Panel Elements

Panel elements are declared in the plugin’s EXTENSIONS dictionary under the panel_elements key:

EXTENSIONS = {
"panel_elements": [
{
"type": "status_display",
"label": "Status Display",
"renderer": "iframe",
"default_size": {"col_span": 3, "row_span": 2},
"config_schema": [
{
"key": "title",
"label": "Title",
"type": "string",
"default": "System Status"
},
{
"key": "state_key",
"label": "State Key",
"type": "state_key",
"default": ""
},
{
"key": "style",
"label": "Display Style",
"type": "select",
"options": ["compact", "detailed", "minimal"],
"default": "compact"
}
]
}
]
}
FieldRequiredDescription
typeYesUnique element type name within this plugin. This also names the renderer file — the panel loads panel/<type>.html (see note below).
labelYesHuman-readable name shown in the Element Palette
rendererYesAlways "iframe"
default_sizeYesDefault grid size when dragged onto the canvas: {"col_span": N, "row_span": N}. The UI Builder uses this on drop and on click-to-add; if you omit it the element falls back to 4×3 cells.
config_schemaNoArray of configuration fields shown in the UI Builder Properties panel. Field types match plugin config (see “Config field types” below): string, text, integer, float, boolean, select, state_key, device_ref, macro_ref. See “Select options” below for static vs. dynamic dropdowns.
sandbox_permissionsNoExtra iframe.sandbox tokens beyond the default allow-scripts. See “Iframe Permissions” below for the whitelist.
allow_featuresNoPermissions-Policy tokens applied via the iframe’s allow attribute. See “Iframe Permissions” below for the whitelist.

Renderer file: the panel loads panel/<type>.html for each element — an element with type: "status_display" loads panel/status_display.html from your plugin directory. Name the HTML file to match the element’s type. (There is no separate URL field; the file name is the contract.)

Config field types: the Properties panel renders each config_schema field by its type, the same way the plugin CONFIG_SCHEMA form does:

TypeControl
stringSingle-line text input
textMulti-line textarea
integer / floatNumber input
booleanCheckbox
selectDropdown (see “Select options” below)
state_keyState-key picker
device_refDevice picker
macro_refMacro picker

Select Options

A select field can populate its dropdown from a static list or from runtime state.

Static options — fixed list set at declaration time:

{"key": "fit", "label": "Fit", "type": "select",
"options": ["contain", "cover"], "default": "contain"}

Dynamic options from state — list driven by a plugin state key. State values must be flat primitives, so the plugin publishes the option list as a JSON-encoded string of [{"value": ..., "label": ...}, ...]:

{"key": "stream_id", "label": "Stream", "type": "select",
"options_source": "plugin.my_plugin.stream_ids"}

And in the plugin code, when the list changes:

self.api.set_state(
"plugin.my_plugin.stream_ids",
json.dumps([{"value": s.id, "label": s.name} for s in streams]),
)

The Properties panel re-renders the dropdown whenever the source state changes. If the currently-selected value isn’t in the published list (e.g. the plugin hasn’t started yet, or the user renamed the option), the stale value still shows as a selectable option so it doesn’t silently switch. This mirrors the convention used by plugin macro action params.

Iframe Permissions

By default, plugin iframes get only sandbox="allow-scripts" and no allow attribute. That’s enough for most widgets — they can run JavaScript and call postMessage back to the panel — but it blocks anything that needs a same-origin context, autoplay-eligible media, or other delegated features. Two optional fields opt into extra permissions; the server filters both against whitelists, so anything outside these tables is silently dropped.

sandbox_permissions — extra iframe sandbox tokens:

TokenWhat it enables
allow-same-originIframe shares your origin; required for fetch against the OpenAVC server with session cookies, or for localStorage
allow-formsForm submission
allow-modalsalert(), confirm(), prompt()
allow-popupswindow.open

Tokens that would let an iframe escape the sandbox (allow-popups-to-escape-sandbox, allow-top-navigation, allow-pointer-lock) are deliberately excluded.

allow-same-origin is a trust grant, not a routine flag. Combined with the always-present allow-scripts, it gives your element’s UI the same origin as the panel page itself — the panel’s storage, its cookies, credentialed fetches against the server — which effectively removes the iframe sandbox boundary in the browser. That is an accepted posture: a plugin is already arbitrary Python running on the server, so the iframe sandbox is defense-in-depth, not the trust boundary. It does mean the grant is part of the trust a user extends when installing your plugin: the Programmer IDE shows each plugin’s sandbox permissions in its details alongside capabilities, and reviewers will expect a plugin declaring allow-same-origin to clearly need it (same-origin fetches or storage). If your element works without it, don’t declare it.

allow_features — Permissions-Policy tokens:

TokenWhat it enables
autoplay<video autoplay muted> works without a user gesture
encrypted-mediaEME (DRM playback)
fullscreenelement.requestFullscreen()
picture-in-picturePiP API

camera, microphone, geolocation, and similar sensor-access tokens are deliberately excluded — no v1 use case needs them, and they have non-obvious privacy implications.

Example: a video-streaming plugin needing same-origin fetches (to a WHEP signaling endpoint hosted on the same OpenAVC server) plus autoplay would declare:

"sandbox_permissions": ["allow-same-origin"],
"allow_features": ["autoplay"]

Stay minimal — only request what you need. Unknown tokens log a warning in the system log so plugin authors can diagnose typos.

File Structure

Plugin panel files (HTML, CSS, JavaScript, images) are served from:

/api/plugins/{plugin_id}/panel/{file_path}

Place your panel files in a panel/ directory inside your plugin folder:

plugin_repo/
└── my_plugin/
├── __init__.py # Plugin class
└── panel/
├── index.html # Main renderer page
├── style.css # Styles
└── app.js # Logic

If your plugin’s Python code spans several files, import the extra modules with a relative import (from . import helpers or from .helpers import Thing). Each plugin loads in its own isolated namespace, so a plain import helpers won’t find a sibling file.

postMessage API

The panel and the plugin iframe communicate through window.postMessage. All messages are JSON objects with a type field.

Panel to iframe (incoming messages):

Message TypeWhen SentPayload
openavc:initWhen the iframe loads, and again whenever the iframe asks via openavc:request-init{config, theme, state, elementId, ext_token}: the element’s plugin_config values, the active theme’s CSS variables, a snapshot of state keys in the plugin’s namespace (plugin.<plugin_id>.*), this element’s ID, and — for plugins that declare ext_auth — a token for calling the plugin’s own /ext/* routes
openavc:stateWhen a key in the plugin’s own namespace changes{key, value}: the changed plugin.<plugin_id>.* key and its new value

The init payload includes a snapshot of the plugin’s own namespace (plugin.<plugin_id>.*) so the iframe can render its current state immediately. openavc:state updates are scoped to that same namespace — a plugin iframe sees only its own state, never other devices’, variables’, or other plugins’ keys.

iframe to panel (outgoing messages):

Outgoing messages use type: "openavc:action" for both device commands and state writes, with an action field selecting the operation. Page navigation uses its own message type.

Message TypeactionPurposePayload
openavc:actiondevice.commandSend a device command{device, command, params}
openavc:actionstate.setWrite a state key{key, value}
openavc:navigateNavigate to a page{page}
openavc:request-initAsk the panel to re-send openavc:init with a freshly-minted ext_token — send it when an /ext/* call starts returning 401 mid-session (the token expired; panels often outlive the token lifetime)

openavc:action requests are gated by the plugin’s declared capabilities, mirroring the server-side checks for Python plugins: device.command requires device_command; state.set to a plugin.<plugin_id>.* key requires state_write; state.set to a var.* key requires variable_write. Writes to device.*, system.*, isc.*, ui.*, another plugin’s namespace, or any action the plugin didn’t declare a capability for are dropped.

Example: Custom Status Display

A minimal panel element that shows a state value with a colored background:

panel/index.html:

<!DOCTYPE html>
<html>
<head>
<style>
body {
margin: 0;
display: flex;
align-items: center;
justify-content: center;
font-family: Inter, sans-serif;
color: #fff;
height: 100vh;
}
.status {
text-align: center;
}
.title { font-size: 12px; opacity: 0.7; }
.value { font-size: 24px; font-weight: bold; }
</style>
</head>
<body>
<div class="status">
<div class="title" id="title">Status</div>
<div class="value" id="value">--</div>
</div>
<script>
let config = {};
window.addEventListener('message', (event) => {
const msg = event.data;
switch (msg.type) {
case 'openavc:init':
config = msg.config || {};
document.getElementById('title').textContent = config.title || 'Status';
document.body.style.backgroundColor = msg.theme?.['--panel-bg'] || '#1a1a2e';
break;
case 'openavc:state':
if (msg.key === config.state_key) {
document.getElementById('value').textContent = msg.value;
}
break;
}
});
</script>
</body>
</html>

Security

Plugin iframes are sandboxed with allow-scripts. This gives the iframe an opaque origin, which means:

  • The iframe can run JavaScript
  • The iframe cannot access the parent panel DOM
  • The iframe cannot read or write cookies, localStorage, or any same-origin resources
  • The iframe cannot navigate the parent page
  • The iframe cannot open popups
  • Network requests are subject to CORS like any cross-origin request

All interaction with the panel goes through the postMessage API. This prevents a plugin from accidentally or intentionally breaking the panel interface.


Contributing Plugins

For the full guide on plugin structure, manifest format, testing, and submitting to the community repository, see the Contributing Plugins guide.


See Also