Plugins
Custom plugins let you extend Athena with your own code. A "card" plugin mounts a custom dashboard card; an "integration" plugin runs server-side JavaScript and reports its own entities; a "subprocess" plugin does the same but runs an executable of your own instead. All three run arbitrary code, so only add ones you trust - Configuration > Plugins is admin-only, same as every other configuration page.
The Plugins list
Each kind gets its own tab, with the same card/list view toggle and search box every other list-view management page already has (see e.g. Automations) - card view is the default. Integration and Subprocess plugins also have an Enabled/Disabled filter, and an Enable/Disable action next to Edit/Delete (in the card-view three-dot menu, or its own button in list view) to pause one without deleting it - Card plugins have no such concept, since one either exists or doesn't. The header's own "+ Add plugin" menu item opens one modal with a Card/Integration/Subprocess selector, defaulting to whichever tab is currently open.
Card plugins
A card plugin is a small ES module, served back to the browser and mounted as a
customElements.define(...) web component - the same mechanism Home Assistant's own
"custom cards" use. Once added here, it shows up as a normal card type when editing a
Dashboard.
// A minimal card plugin - shows a static greeting
class HelloCard extends HTMLElement {
connectedCallback() {
this.innerHTML = '<div style="padding:1rem">Hello from a card plugin!</div>';
}
}
customElements.define('hello-card', HelloCard);
Integration plugins
An integration plugin is a JavaScript program that runs continuously on the server (embedded in Athena itself, not a subprocess) and reports its own entities into the same entity registry every built-in integration uses - it shows up in Devices & Services' own Devices/Entities tabs, in History, on a Dashboard, and as a source in Automations, exactly like a native integration would.
A script sees one global, athena:
athena.config- this plugin's own configuration, entered as JSON when adding it and already parsed for the script.athena.reportEntity({ key, name, domain, state, unit, deviceClass, stateClass })- upserts one entity's current value; everything butkeyandstateis optional.athena.log(message)- writes to Athena's own log, tagged with the plugin's name.athena.fetch(url, options)- a real HTTP client, returns aPromise<{ status, body }>;optionsis{ method, headers, body }, all optional (defaults to a plain GET).athena.mqtt.subscribe(topic, callback)- subscribes to any MQTT topic over Athena's own already-configured broker connection (the same one every discovered MQTT entity uses - no separate connection details to enter here);callback(payload)is called with each message's raw payload as a string, so a script that expects JSON callsJSON.parse(payload)itself. This is the natural way to bridge, say, a Home Assistantmqtt.publishautomation into Athena's own entity registry.
Full setTimeout/setInterval/async/await
support comes for free, backed by a real JavaScript event loop - a typical script sets up one
setInterval that fetches something and calls athena.reportEntity, or an
athena.mqtt.subscribe callback that does the same whenever a message arrives.
A State Trigger in Automations sourced from an integration
plugin's own entity fires the moment athena.reportEntity is called for it - not just
whenever something else happens to re-read the entity list.
// A minimal integration plugin - reports a random temperature every 30s
setInterval(() => {
const value = 18 + Math.random() * 4;
athena.reportEntity({
key: 'demo_temperature',
name: 'Demo Temperature',
domain: 'sensor',
state: value.toFixed(1),
unit: '°C',
deviceClass: 'temperature',
stateClass: 'measurement'
});
}, 30000);
Integration plugins are read-only in this first cut - a reported entity can't be commanded from a Dashboard or Automation yet. That's a natural next step once this proves out, not part of the initial release.
Subprocess plugins
A subprocess plugin runs an executable of your own as a real OS process instead of a JavaScript module - for an integration the embedded JS engine genuinely can't reach: raw serial port access, native crypto, calling into some other language's own SDK, that kind of thing. It reports entities into the exact same registry an integration plugin does, so it shows up everywhere an integration plugin's entities do.
A brand-new subprocess plugin has to be saved once (name + JSON config) before there's anywhere to attach an executable to - the Edit modal then gains an Executable section with a file picker and an Upload button. Re-uploading replaces the previous executable outright; there's exactly one "current" binary per plugin.
The wire protocol is deliberately simple - any language that can print a line to stdout can implement it, no SDK or code generation required:
- On start, Athena writes one line to the process's own stdin:
{"config": {...}}, holding the plugin's own parsed config. - From then on, Athena reads the process's own stdout one line at a time. Each line is a JSON
object with a
"type"field:"entity"- upserts one entity's current value; everything butkeyandstateis optional:{"type":"entity","key":"...","name":"...","domain":"...","state":"...","unit":"...","deviceClass":"...","stateClass":"..."}{"type":"log","message":"..."}- written to Athena's own log.
- The process's own stderr is captured and logged verbatim - the place for a plugin's own panics/stack traces/debug output.
#!/usr/bin/env python3 # A minimal subprocess plugin - reports uptime every 30s import json, sys, time config = json.loads(sys.stdin.readline()) # {"config": {...}} while True: print(json.dumps({ "type": "entity", "key": "demo_uptime", "name": "Demo Uptime", "domain": "sensor", "state": int(time.time()), "unit": "s" }), flush=True) time.sleep(30)
Subprocess plugins are read-only in this first cut, same as integration plugins - a reported
entity can't be commanded from a Dashboard or Automation yet, and a crashed process isn't
automatically restarted (disabling and re-enabling the plugin, or restarting Athena, starts it
again). A State Trigger sourced from a subprocess plugin's own entity fires the same way an
integration plugin's does - the moment an "entity"-type stdout line for it arrives.
Uploaded executables are stored under PLUGIN_BIN_DIR
(./plugin-bins by default), bind-mounted the same way the log directory is - see
Installation & deployment.