BusDiagram GitHub Download

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.