Abstract Class: Plugin<Config>
The class you write once per integration: it connects to one device or service and offers its sensors, actuators and actions to Nexus. One instance runs per configured plugin instance, in its own worker thread, so a plugin that crashes or hangs affects nothing else. Extend it, declare the static fields (see PluginClass), and export the class with PluginEntry.define.
What you override: start() (begin work; usually register a poller with every()), and
optionally catalog(), panel(), watched(), watchChanged() and unhandledRejection().
What you read: config, log, store, status, clock, timeZone, http, slug, watch.
What you call: panelChanged(), and from Disposable own(), every(), after() and
listen(), which are cleaned up when the instance stops. Sensors and actuators reach the plugin
through their this.plugin, so state they share (an API client, the latest reading) lives here.
The order of events for one run: the constructor (field initializers can already use
this.config), then start(), then the sensors and actuators the user configured (each gets its
own start()), then requests such as catalog(), panel() and webhooks for as long as it runs.
When the instance stops, its signals are disposed and then the plugin. A crash or a restart
creates a new object; nothing in memory survives, only the store does.
Plugin code can throw into the host only through start() and the request methods; everything else is logged or, for a promise nobody handles, given to unhandledRejection().
Example
A plugin that polls a smart plug and shares each reading with its sensors:
export class SmartPlugPlugin extends Plugin<SmartPlugConfig> {
static readonly config = smartPlugConfig
static readonly sensors = [MeterSensor]
static readonly actuators = [SwitchActuator]
readonly client = new PlugClient(this.config.address)
readonly readings = new EventEmitter()
override start() {
void this.poll()
this.every(this.config.pollSeconds * 1000, () => void this.poll())
}
// called with `void`, so it must not reject: an unreachable plug degrades the instance
async poll() {
try {
this.readings.emit("reading", await this.client.status())
this.status.set("running")
} catch (error) {
this.status.set("degraded", error instanceof Error ? error.message : String(error))
}
}
}
Extends
Type Parameters
Config
Config = unknown
Constructors
Constructor
new Plugin<
Config>(context):Plugin<Config>
Field initializers of subclasses run after this, so they can already use this.config.
Parameters
context
Returns
Plugin<Config>
Overrides
Properties
MAX_TIMER_MS
readonlystaticMAX_TIMER_MS:2147483647=2_147_483_647
the largest delay setTimeout/setInterval honour; Node turns anything longer into 1 ms
Inherited from
context
protectedreadonlycontext:PluginContext
Accessors
isDisposed
Get Signature
get isDisposed():
boolean
true from the moment dispose() starts, not when it finishes
Returns
boolean
Inherited from
config
Get Signature
get config():
Config
The instance config, already validated against the static config schema (defaults applied).
It never changes during one run: when the user saves new settings, the instance restarts and
a new plugin object is built with them.
Returns
Config
clock
Get Signature
get clock():
Clock
The time to use instead of Date.now(): this.clock.now(). every() and after() run on it too.
The system clock in production; a TestClock when a test gives one.
Returns
timeZone
Get Signature
get timeZone():
string
The IANA time zone of the installation, e.g. "Europe/Vienna", for anything that depends on the local time (opening hours, "today"). Nexus takes it from the Loxone project; without one it is the server's zone, which in a container is often UTC; so is a zone this runtime doesn't know. Fixed for one run of the instance.
Returns
string
log
Get Signature
get log():
PluginLogger
Writes to the server log, tagged with this instance, and to the instance's page (its last lines). Inside an actuator's run() a line is also linked to the command that caused it. Never log secrets.
Returns
logLevel
Get Signature
get logLevel():
PluginLogLevel
Which log lines the host keeps: current is "debug" while the user switched on debug logging
for this instance, while the server's own log is at debug, and always with older hosts; "info"
otherwise. For raising a library's own verbosity along with it; plain log.debug() calls need
nothing, they are dropped cheaply while debug is off.
Returns
store
Get Signature
get store():
PluginStore
Small persisted state of this instance, for what must survive a restart (a token, a cursor, the id last seen). Reads are synchronous; writes are saved by the host in the background.
Returns
status
Get Signature
get status():
PluginStatus
Reports the plugin's own health to the UI: "degraded" with a message while something is wrong but the plugin keeps trying, "running" when it is fine again. A new run starts as "running".
Returns
slug
Get Signature
get slug():
string|undefined
The instance's slug, e.g. to show its webhook URL (/hook/<slug>/…); undefined with hosts that
don't send it. A slug change restarts the instance, so it is fixed for one run.
Returns
string | undefined
http
Get Signature
get http():
PluginHttp
Registers webhook routes under /hook/<slug>/…. Register them in start(); routes are
unauthenticated, so the handler must check a secret itself (see PluginHttp).
Returns
watch
Get Signature
get watch():
WatchView
The signals the user chose for this instance, for plugins that declare watches. Empty for
other plugins and until the host has sent the list (it arrives after start() resolved, and
again whenever the choice changes). Read it inside watched() and watchChanged(), not in
start().
Returns
Methods
dispose()
dispose():
Promise<void>
Releases everything registered with this object. Plugin code seldom needs it: the runtime disposes plugins, sensors and actuators, and own() disposes children. Call it yourself for an object you created without own(), or to end a child early.
Runs all teardowns in reverse registration order and keeps going when one fails. Idempotent: every call returns the same promise, so a second caller also waits for async teardowns.
Returns
Promise<void>
Throws
AggregateError collecting every teardown error, after all teardowns have run
Inherited from
onDispose()
protectedonDispose(teardown):void
Registers a callback that runs on dispose(), for cleanup the helpers don't cover (closing a socket, flushing a buffer). Async callbacks are awaited one after the other, last registered first, so a flush can use what was registered before it. Keep the total under the five seconds a stop has.
Parameters
teardown
Returns
void
Throws
Error if already disposed
Inherited from
useClock()
protecteduseClock(clock):void
Sets the clock for every() and after() calls from now on, unless one is set already; without one they run on the system clock. Timers started before keep the clock they started on. Plugin, Sensor and Actuator set it from the plugin's context.
Parameters
clock
Clock | undefined
Returns
void
Inherited from
currentClock()
protectedcurrentClock():Clock
The clock this object runs on: every() and after() use it, and a Disposable that isn't a
plugin, sensor or actuator reads the time from it (this.currentClock().now()).
Returns
Inherited from
own()
protectedown<T>(resource):T
ties the lifetime of a child resource to this object; returns it so construction and registration fit in one expression. A Disposable child without a clock of its own runs on this one's, looked up when a timer starts, so a chain of owners (plugin → client → poller) shares the plugin's clock. Timers a child starts in its constructor, before it is owned, run on the system clock.
Type Parameters
T
T extends DisposableLike
Parameters
resource
T
Returns
T
Inherited from
every()
protectedevery(ms,fn): () =>void
runs fn every ms milliseconds until disposed or until the returned cancel function is called. The first run is after ms, not at once; call fn yourself first if you want an immediate run. Runs are not queued: a slow async fn can overlap its next run, so guard it yourself if needed.
Parameters
ms
number
fn
() => void
Returns
() => void
Throws
RangeError unless 0 < ms <= MAX_TIMER_MS
Inherited from
after()
protectedafter(ms,fn): () =>void
runs fn once after ms milliseconds unless disposed before; returns a cancel function. Both firing and cancelling forget the teardown, so repeated use does not accumulate entries. Delays beyond MAX_TIMER_MS are scheduled in steps, so they don't fire early.
The delay counts elapsed time, not wall-clock time: a clock step (an NTP sync after boot) is not noticed. For a deadline given as a wall-clock time, wait in shorter slices and re-check the time each time.
Parameters
ms
number
fn
() => void
Returns
() => void
Throws
RangeError for negative or non-finite delays
Inherited from
listen()
protectedlisten(emitter,event,listener):void
Subscribes listener to event of an EventEmitter (or anything with on/off) and removes it
again on dispose(), so a long-lived emitter such as the plugin's poller does not keep a
disposed sensor alive. Prefer it to calling emitter.on yourself.
Parameters
emitter
event
string | symbol
listener
Returns
void
Inherited from
start()
start():
void|Promise<void>
Starts the work. Called once per run, after the constructor and before the first signal is
added; the host waits for it to resolve (at most 30 seconds, then it terminates the worker and
restarts it like after a crash). Reject only for problems a restart can't fix (invalid
credentials, a missing device): the instance goes into the error state, what start()
registered is disposed, and it is not restarted. For a device that is merely unreachable,
resolve and report status.set("degraded", ...) while retrying. Slow first requests belong in
the background (void this.poll()), not in an awaited start().
Returns
void | Promise<void>
catalog()
catalog():
CatalogNode[] |Promise<CatalogNode[]>
Optional tree of ready-to-add sensors and actuators, e.g. built from the device's data. Asked on demand by the UI, so it may query the device; a throw is shown to the user as the error. It must answer within 30 seconds and with at most 16 MiB of JSON, or the request fails. Can be called at any time while the plugin runs, also several times at once.
Returns
CatalogNode[] | Promise<CatalogNode[]>
panel()
panel(): {
type:"keyValue";items:object[]; } | {type:"table";columns:string[];rows: (string| {text?:string;tone?:"error"|"ok"|"warn"|"info"|"muted";until?:number;since?:number;at?:number; })[][];rowActions?:object[][];rowCatalog?: (string|null)[]; } | {type:"qr";data:string;caption?:string; } | {type:"json";data:unknown; } | {type:"tiles";tiles:object[]; } | {type:"notice";tone:"error"|"ok"|"warn"|"info"|"muted";title?:string;text:string;link?: {label:string;href:string; }; } | {type:"sections";sections:object[]; } |Promise<{type:"keyValue";items:object[]; } | {type:"table";columns:string[];rows: (string| {text?:string;tone?:"error"|"ok"|"warn"|"info"|"muted";until?:number;since?:number;at?:number; })[][];rowActions?:object[][];rowCatalog?: (string|null)[]; } | {type:"qr";data:string;caption?:string; } | {type:"json";data:unknown; } | {type:"tiles";tiles:object[]; } | {type:"notice";tone:"error"|"ok"|"warn"|"info"|"muted";title?:string;text:string;link?: {label:string;href:string; }; } | {type:"sections";sections:object[]; } |undefined> |undefined
Optional status view for the plugin's page; undefined shows nothing. The page asks for it every
few seconds while it is open, so keep it cheap and read from memory. A plugin that declares
static panelUpdates = "push" calls panelChanged() instead, and is asked when it does (and
otherwise only every half minute). A throw or an invalid panel fails the request; the same 30
seconds and 16 MiB limits as catalog() apply. Only asked while the instance runs.
Returns
Type Literal
{ type: "keyValue"; items: object[]; }
Type Literal
{ type: "table"; columns: string[]; rows: (string | { text?: string; tone?: "error" | "ok" | "warn" | "info" | "muted"; until?: number; since?: number; at?: number; })[][]; rowActions?: object[][]; rowCatalog?: (string | null)[]; }
type
type:
"table"
columns
columns:
string[]
rows
rows: (
string| {text?:string;tone?:"error"|"ok"|"warn"|"info"|"muted";until?:number;since?:number;at?:number; })[][]
rowActions?
optionalrowActions?:object[][]
rowCatalog?
optionalrowCatalog?: (string|null)[]
per row, the catalog node with that row's signals (see CatalogRef), or null for none
Type Literal
{ type: "qr"; data: string; caption?: string; }
Type Literal
{ type: "json"; data: unknown; }
Type Literal
{ type: "tiles"; tiles: object[]; }
Type Literal
{ type: "notice"; tone: "error" | "ok" | "warn" | "info" | "muted"; title?: string; text: string; link?: { label: string; href: string; }; }
Type Literal
{ type: "sections"; sections: object[]; }
Promise<{ type: "keyValue"; items: object[]; } | { type: "table"; columns: string[]; rows: (string | { text?: string; tone?: "error" | "ok" | "warn" | "info" | "muted"; until?: number; since?: number; at?: number; })[][]; rowActions?: object[][]; rowCatalog?: (string | null)[]; } | { type: "qr"; data: string; caption?: string; } | { type: "json"; data: unknown; } | { type: "tiles"; tiles: object[]; } | { type: "notice"; tone: "error" | "ok" | "warn" | "info" | "muted"; title?: string; text: string; link?: { label: string; href: string; }; } | { type: "sections"; sections: object[]; } | undefined>
undefined
panelChanged()
panelChanged():
void
Tells open pages that panel() would now show something else, e.g. a device was paired. Cheap
to call often: the host hears about it at most once a second. Does nothing on hosts that
don't take it; declare static panelUpdates = "push" so pages rely on it.
Returns
void
watched()?
optionalwatched(values,dropped):void|Promise<void>
Values of the watched signals (plugins that declare watches), oldest first, in batches of up
to a thousand. The next batch comes once this returns (or its promise settles), so a slow
export slows delivery instead of piling up; meanwhile the host keeps a bounded queue and
dropped says how many values it had to leave out since the last batch. A throw is logged
and the feed goes on. Calls never overlap, and a running call finishes before the plugin is
disposed on stop. Declare static watches too, or the plugin is never given any values.
Parameters
values
readonly object[]
dropped
number
Returns
void | Promise<void>
watchChanged()
watchChanged():
void
The watched signals changed: chosen or left out, renamed. this.watch has the new list.
Called once after start() with the first list, then on every change; always before the
values that belong to the new list. Optional to override.
Returns
void
unhandledRejection()
unhandledRejection(
reason):void
A promise in this worker was rejected and nobody handled it (a forgotten .catch). The worker
keeps running; by default the reason is logged as an error with its stack. Override it to say
more, e.g. which line of user code it came from, or to report a degraded status. It must not
throw: if it does, the original reason is logged instead. Not called before the plugin is
created or after it is disposed; the runtime logs those.
Parameters
reason
unknown
Returns
void