Guide
#Extensions
An extension can add a behavior, an equipment model, or a view without changing the library bundle. Load its script after bus-diagram.js, then refer to the registered identifier in a scenario. See the complete example.
#Register a behavior
The sample site/samples/extensions/delayed-switch.ts defines a delayed switch:
export const delayedSwitch: BehaviorDefinition<State> = {
description: "Switch with delayed turn-on and immediate turn-off.",
channelParameters: {
type: "object",
additionalProperties: false,
properties: {
delayMs: {
title: "Turn-on delay",
unit: "ms",
type: "integer",
minimum: 0,
default: 2000,
description: "Turn-on delay (ms).",
},
},
},
ports: {
switch: {
dpts: ["1.001"],
channel: "required",
title: "Command",
direction: "in",
},
status: {
dpts: ["1.001"],
channel: "required",
title: "Status feedback",
direction: "out",
},
},
output: "switch",
createState: (d) => ({
channels: Object.fromEntries(
d.channels.map((c) => [c.id, { on: false, onAtMs: null }]),
),
}),
onInit(ctx) {
ctx.device.channels.forEach((c) =>
ctx.setOutput(c.id, { type: "switch", on: false }),
);
},
onObjectWrite(ctx, e) {
const o = ctx.device.objects.find((x) => x.id === e.objectId);
const ch = o?.channel;
if (!o || o.port !== "switch" || !ch) return;
const st = ctx.state.channels[ch]!;
if (e.newValue) {
const delay = Number(
ctx.device.channels.find((c) => c.id === ch)?.parameters.delayMs ??
2000,
);
ctx.schedule(`${ch}:on`, delay); // replaces a pending timer with the same key
st.onAtMs = ctx.timeMs + delay;
ctx.note(ctx.t`${ch}: scheduled to switch on in ${delay / 1000} s`);
} else {
ctx.cancel(`${ch}:on`);
st.onAtMs = null;
setRelay(ctx, ch, false);
}
},
onTimer(ctx, key) {
const [ch, what] = key.split(":");
if (!ch) return;
if (what === "on") {
ctx.state.channels[ch]!.onAtMs = null;
setRelay(ctx, ch, true);
} else if (what === "status")
ctx.device.objects
.filter((o) => o.port === "status" && o.channel === ch)
.forEach((o) => ctx.transmit(o.id));
},
channelState: (state, ch) => ({ ...(state.channels[ch] ?? {}) }),
};
registerBehavior("delayedSwitch/v1", delayedSwitch);A behavior may implement these entry points:
| Entry point | Called when |
|---|---|
onInit(ctx) |
A device is created; initialize outputs without transmitting. |
onInput(ctx, input) |
A local press, short, long, or value action occurs. |
onObjectWrite(ctx, event) |
An incoming write is accepted by an object with flag W, even if the value has not changed. |
onTimer(ctx, key, payload) |
An event scheduled with ctx.schedule is due. |
onTick(ctx, dtMs) |
Simulation time advances; called every 20 ms when implemented. |
onRoomChange(ctx, room) |
The device's room temperature or window state changes. |
onClockChange(ctx) |
The simulated clock is set to another time; reschedule clock-based deadlines. |
channelState(state, channel) |
Channel state is requested by the inspector or getState(). |
deviceState(state) |
Device state is requested, such as a thermostat display. |
The context ctx exposes t for translated messages, timeMs, device, state, getObject, setObject, transmit, setOutput, getOutput, readEquipment, readPower, readRoom, setOutsideTemperature, clock, schedule, cancel, and note. setObject changes a local value without transmitting. transmit checks flag T and the sending address. readPower returns the power drawn by a channel's load, in W, when the load models it. clock() returns the simulated clock (nowMs, speed) or null without one; divide a clock delay by speed to schedule it in simulation time. note adds an explanation to the event log.
Equipment models can define heatOutput(state, parameters) to heat or cool a room and checkParameters(parameters, t) to validate related parameters. A view can define interact(state, action, parameters, equipment) to respond to a user action. Use simulation time and ctx.schedule for delayed behavior; browser timers such as setTimeout do not follow simulation time.
An exception in a behavior pauses only that diagram and reports an extension-error. An exception in a view replaces that view with an error frame and reports view-error in getState().
#Definition checks
registerBehavior validates ports, supported DPTs, output types, and parameter defaults. Parameter schemas use a flat subset of JSON Schema: scalar integer, number, boolean, string, and null properties, with bounds, enum, enumTitles, default, and description. Nested objects and arrays are unsupported. A registered definition is copied and frozen.
#Labels for guided editing
Optional metadata makes an extension usable in the guided designer:
| Field | Effect |
|---|---|
Parameter title |
Label shown instead of the property name. |
Parameter unit: "ms" or "%" |
Display seconds for millisecond values or show a percent sign. |
Parameter expert: true |
Place the control under advanced settings. |
Parameter enumTitles |
Labels for enum values, in the same order. |
Parameter nullTitle |
Label for null; otherwise the designer shows “none”. |
Port title |
Name shown for the communication object. |
Port direction: "in" or "out" |
Choose the default W or T flag. |
Equipment title |
Name of the connected load. |
Use ctx.t for messages in the diagram's selected language. Supply extension translations with BusDiagram.registerMessages(language, messages); see Languages.
#Register a view
BusDiagram.registerEquipmentView("ledStrip", {
size: { width: 84, height: 30 },
render: ({ state, label, box }) => BusDiagram.html`
<div style="position:absolute;left:${box.x}px;top:${box.y}px">${state.on ? "●" : "○"} ${label}</div>`,
});A scenario can select this view with "equipment": { "type": "lamp", "view": "ledStrip" }. The underlying lamp model and its { on } state remain the same.
#Load scripts
<script defer src="bus-diagram.js"></script>
<script defer src="extensions/delayed-switch.js"></script>Components wait for these scripts to load. Extensions share registered definitions through BusDiagram; they should not rely on global variables from another extension. The designer and standalone export wrap each extension in its own function scope. When loading raw files with <script src>, compile each extension as an IIFE to avoid global name collisions. npm run build compiles site/samples/extensions/*.ts into classic scripts.