@enms-vis/enms-vis-create
v2.0.2
Published
CLI tool to bootstrap enms-vis visualization projects
Readme
@enms-vis/enms-vis-create
CLI scaffolding tool for enms-vis SVG visualization projects.
Table of Contents
- Getting Started
- Tools & Packages
- API Reference & Examples
- Using Without Vue
- Manifest Format (
enms-plugin-manifest.json) - Building for enms-web
Getting Started
1. Create a new project
npx @enms-vis/enms-vis-create my-visualizationThe CLI prompts for a project name (if not provided) and scaffolds a ready-to-run Vue + Vite project with an example SVG visualization.
2. Install dependencies
cd my-visualization
npm install3. Start the development server
npm run devVite starts a local dev server with hot-reload. Open the printed URL in your browser to see the visualization. The plugin runs in standalone mode when opened outside of an enms-web iframe – a local mock connection is created automatically so you can develop without a running enms-web instance.
4. Edit the visualization
The scaffolded project contains:
| Path | Purpose |
|---|---|
| src/components/SvgVisualization.vue | Main Vue component – links your SVG as a template |
| src/assets/visualization.svg | SVG artwork – use Vue template syntax directly inside it |
| src/assets/visualization.css | Styles scoped to the visualization |
| enms-plugin-manifest.json | Declares inputs the visualization expects from enms-web |
Open the SVG in Inkscape (or any editor), add shapes, then use Vue bindings ({{ }}, v-on:click, :fill, etc.) via the XML editor to connect them to live data.
5. Build for deployment
npm run buildThe output in dist/ together with enms-plugin-manifest.json forms the deployable plugin package (see Building for enms-web).
Tools & Packages
| Package | Purpose |
|---|---|
| @enms-vis/enms-vis-create | CLI tool that scaffolds new visualization projects. Generates a preconfigured Vue + Vite project with an example SVG, manifest, and all necessary wiring. |
| @enms-vis/enms-web-plugin | Framework-agnostic communication library. Handles the iframe ↔ host messaging protocol (input delivery, patching, click events) between a visualization and enms-web. |
| @enms-vis/enms-web-plugin-vue | Vue integration layer. Provides the useWebPlugin() composable that wraps @enms-vis/enms-web-plugin into reactive Vue state, lifecycle management, and convenient helpers. |
API Reference & Examples
useWebPlugin
The main entry point. Call it inside a Vue component's setup() to connect to the enms-web host and get reactive access to input data.
function useWebPlugin<TState = unknown>(pluginName?: string, options?: UseWebPluginOptions): UseWebPluginReturn<TState>Best practice: Always pass a
pluginNameand declare theTStatetype parameter when your plugin persists state. ThepluginNameis used as thelocalStoragekey prefix in standalone mode and appears in log messages;TStategives you full type-safety forpluginStateandstateStorage.saveState().
<script lang="ts">
import { defineComponent } from 'vue'
import { useWebPlugin } from '@enms-vis/enms-web-plugin-vue'
type PluginState = { zoom: number }
export default defineComponent({
name: 'SvgVisualization',
setup() {
// Recommended: explicit TState + pluginName
const webPlugin = useWebPlugin<PluginState>('SvgVisualization');
return {
...webPlugin
}
},
})
</script>Options (UseWebPluginOptions)
options is an optional object that controls both the underlying host connection and the built-in editor button overlay.
| Option | Type | Default | Description |
|---|---|---|---|
| editButton | boolean \| HTMLElement \| undefined | undefined | Controls when the Input Payload Editor button overlay is mounted. See table below. |
| allowedOrigins | string[] | ['*'] | Allowed origins for postMessage communication. Restrict this in production for extra security (e.g. ['https://your-enms-host.example.com']). |
| timeout | number | 2000 | Host connection timeout in milliseconds. If the handshake with the parent window does not complete within this time, the plugin falls back to standalone mode. |
editButton behaviour:
| Value | Connected (iframe) mode | Standalone / detached mode |
|---|---|---|
| undefined (default) | Auto – shown only while edit mode is active | Always shown |
| false | Never shown | Never shown |
| true | Always shown (ignores edit-mode state) | Always shown |
| HTMLElement | Like true, but mounted inside the given element | Like true, but mounted inside the given element |
<script lang="ts">
import { defineComponent } from 'vue'
import { useWebPlugin } from '@enms-vis/enms-web-plugin-vue'
export default defineComponent({
name: 'SvgVisualization',
setup() {
return {
...useWebPlugin(
'SvgVisualization',
{
editButton: false, // never show the editor button
allowedOrigins: ['https://enms.example.com'],
timeout: 3000,
},
)
}
},
})
</script>useWebPlugin() returns:
| Property | Type | Description |
|---|---|---|
| V | PluginStateValues | Reactive map of input data keyed by inputId |
| debugMode | Ref<boolean> | Whether the host enabled debug mode |
| editMode | Ref<boolean> | Whether the dashboard is currently in edit mode |
| isStandalone | Ref<boolean> | Whether the plugin is running without a host connection (e.g. in dev mode) |
| widgetName | Ref<string \| null> | Human-readable name of the widget instance hosting the plugin |
| enmsVersion | Ref<string \| null> | Version string of the ENMS application |
| pluginState | Ref<TState \| null \| undefined> | Last plugin state received from the host; null = nothing saved yet; undefined = not yet received |
| stateStorage | ShallowRef<StateStorage<TState> \| null> | Abstraction for saving and loading plugin state and editor saved-states |
| emitClick | Function | Emit a click event to the host |
| pluginLog | Function | Plugin-scoped logging (debug logs only when debugMode is on) |
| deviceParam | Function | Get a single parameter from a device input |
| mockClient | WebPluginClient \| null | Available only in standalone/dev mode for pushing mock data |
Reactive State (V)
The V object is a reactive map whose keys match the inputId values declared in enms-plugin-manifest.json. Use it directly in SVG templates:
<!-- Display device name -->
<text x="60" y="114" font-family="sans-serif" font-size="20">
{{ V?.device1?.name ?? '–' }}
</text>
<!-- Display a parameter value with unit -->
<text x="60" y="220" font-family="monospace" font-size="22">
{{ V?.device1?.params?.active_power?.currentValue?.valueEnd?.toFixed(2) ?? '–' }}
{{ V?.device1?.params?.active_power?.currentValue?.valueUnit ?? '' }}
</text>deviceParam Helper
deviceParam(deviceInputId, paramCodeOrInputId) is a shorthand for accessing a single parameter from a device input. It returns a ParamInputPayload or null.
const param = this.deviceParam('ekostat_1', 'temperature_actual');
// param?.currentValue?.valueEnd → numeric value
// param?.currentValue?.valueUnit → unit string (e.g. '°C')
// param?.code → parameter code
// param?.name → parameter nameComputed Properties Pattern
For cleaner templates, define Vue computed properties that use deviceParam to extract and format values. This keeps the SVG template readable and the formatting logic in one place.
<script lang="ts">
import { defineComponent } from 'vue'
import { useWebPlugin } from '@enms-vis/enms-web-plugin-vue'
export default defineComponent({
name: 'SvgVisualization',
setup() {
const webPlugin = useWebPlugin();
return {
...webPlugin
}
},
computed: {
ekostat_1_ts() {
const param = this.deviceParam('ekostat_1', 'current_temperature_setpoint');
return (param?.currentValue?.valueEnd?.toFixed(2) ?? '---')
+ (param?.currentValue?.valueUnit ?? '');
},
ekostat_1_ta() {
const param = this.deviceParam('ekostat_1', 'temperature_actual');
return (param?.currentValue?.valueEnd?.toFixed(2) ?? '---')
+ (param?.currentValue?.valueUnit ?? '');
},
ekostat_1_state() {
const param = this.deviceParam('ekostat_1', 'relay_state');
return (param?.currentValue?.valueEnd ?? '---')
},
},
})
</script>Then reference them in the SVG template:
<text x="100" y="80" font-family="monospace" font-size="16">
Setpoint: {{ ekostat_1_ts }}
</text>
<text x="100" y="110" font-family="monospace" font-size="16">
Actual: {{ ekostat_1_ta }}
</text>
<text x="100" y="140" font-family="monospace" font-size="16">
Relay: {{ ekostat_1_state }}
</text>The corresponding enms-plugin-manifest.json for the example above uses deviceParams to load all three parameters at once by their codes:
{
"inputs": [
{
"inputId": "ekostat_1",
"type": "device",
"inputName": { "pl": "Urządzenie ekostat" },
"valueSpec": {
"type": "device",
"params": [
{
"type": "deviceParams",
"paramDsl": "param_code = current_temperature_setpoint,temperature_actual,relay_state",
"valueSpec": {
"type": "param",
"currentValue": {
"value": true
}
}
}
]
}
}
]
}With deviceParams, matching parameters are automatically keyed by their parameter code in the payload, so deviceParam('ekostat_1', 'temperature_actual') works without declaring each parameter separately (see Manifest Format for details).
emitClick
emitClick(action, input, mouseEvent) sends a click event to the enms-web host. The host interprets the action and the input payload to perform a context-specific response (e.g. opening a tooltip or navigating to a detail page).
Parameters:
| Parameter | Type | Description |
|---|---|---|
| action | PluginEventAction | Action for the host to perform. Currently supported: 'open-tooltip' |
| input | InputPayload \| InputPayload[] \| null | The input payload associated with the click. Determines what the host shows (e.g. which device/parameter tooltip). Pass a device payload for device-level actions or a parameter payload for parameter-level actions. |
| mouseEvent | MouseEvent | The native DOM mouse event ($event in Vue templates). Used to position the tooltip at the click location. |
Example – open tooltip for a device:
When the user clicks a shape, the host opens a tooltip showing details for the device:
<rect x="40" y="60" width="320" height="90"
style="cursor: pointer"
v-on:click="emitClick('open-tooltip', V?.device1, $event)" />Example – open tooltip for a specific parameter:
Clicking a parameter value area opens a tooltip for that specific parameter:
<rect x="40" y="168" width="148" height="80"
style="cursor: pointer"
v-on:click="emitClick('open-tooltip', V?.device1?.params?.active_power, $event)" />Example – using deviceParam in a click handler:
<rect x="40" y="168" width="148" height="80"
style="cursor: pointer"
v-on:click="emitClick('open-tooltip', deviceParam('ekostat_1', 'temperature_actual'), $event)" />The call is safe even when the host is not connected – if the plugin is running in standalone mode, emitClick is silently ignored.
Plugin State
Plugins can persist arbitrary state across page reloads and widget reconfigurations using the stateStorage / pluginState API. This is useful for things like zoom levels, user preferences, or any configuration data that should survive a page refresh.
Saving state
Call stateStorage.value?.saveState(state) with any serializable object. In connected mode the state is persisted via the ENMS host; in standalone mode it falls back to localStorage.
It is good practice to guard saves so they only happen when the plugin is in edit mode or standalone mode, preventing unintended changes during normal view-mode operation:
import { defineComponent } from 'vue'
import { useWebPlugin } from '@enms-vis/enms-web-plugin-vue'
type PluginState = { zoom: number; activeTab: string }
export default defineComponent({
name: 'SvgVisualization',
setup() {
return { ...useWebPlugin<PluginState>('MyPlugin') }
},
methods: {
saveCurrentState() {
if (!this.editMode && !this.isStandalone) return; // view-only guard
this.stateStorage?.saveState({ zoom: 2, activeTab: 'overview' });
},
},
})Reading state
pluginState is a computed Ref that always reflects the latest value from the active storage backend. It has three possible values:
| Value | Meaning |
|---|---|
| undefined | Not yet received – waiting for the first host config delivery. |
| null | Received; no state has been saved yet. |
| TState | The previously saved state object. |
React to state changes with a watcher:
watch: {
pluginState: {
handler(state: PluginState | null | undefined) {
if (!state) return; // null or undefined – nothing to restore
this.pluginLog('info', 'Restoring state:', state);
// apply state to your visualisation...
},
immediate: true, // run once the first value is delivered
},
},Or read it synchronously inside computed properties:
computed: {
currentZoom() {
return this.pluginState?.zoom ?? 1;
},
},Storage backends
stateStorage is a ShallowRef that switches backend automatically depending on the connection status:
| Mode | Backend | Where state lives |
|---|---|---|
| Connected (iframe) | HostStateStorage | ENMS host saveState API |
| Standalone / dev | LocalStateStorage | Browser localStorage |
stateStorage is null before the first connection attempt completes (i.e. briefly during the initial onMounted). Always guard with ?. when accessing it outside of lifecycle callbacks.
Runtime Info, Debug Mode & Logging
useWebPlugin exposes several reactive properties that reflect the current runtime context delivered by the host:
| Property | Type | Description |
|---|---|---|
| debugMode | Ref<boolean> | true when the host has enabled debug mode for this widget. |
| editMode | Ref<boolean> | true when the dashboard is in edit mode. Use this to guard state saves or show configuration overlays. |
| isStandalone | Ref<boolean> | true when the plugin is running outside of an ENMS host iframe (e.g. during local development). |
| widgetName | Ref<string \| null> | Human-readable name of the widget instance as configured in the dashboard. Useful for labelling or logging. |
| enmsVersion | Ref<string \| null> | Version string of the running ENMS application. Useful for diagnostics. |
You can use these properties directly in templates:
<!-- Show a configuration hint when in edit mode -->
<text v-if="editMode" x="10" y="20" font-size="12" fill="blue">
Edit mode – configure inputs in the sidebar
</text>
<!-- Show widget name as a title -->
<text x="10" y="40" font-size="14">{{ widgetName ?? 'Unnamed widget' }}</text>
<!-- Show ENMS version in a debug overlay -->
<text v-if="debugMode" x="10" y="310" font-size="9" fill="grey">
enms {{ enmsVersion }} · standalone: {{ isStandalone }}
</text>Use pluginLog for diagnostics. Debug-level messages are only printed when the host enables debug mode.
this.pluginLog('info', 'Initialization complete');
this.pluginLog('debug', 'Current state:', this.V);
this.pluginLog('warn', 'Missing expected input');You can conditionally render debug overlays in the SVG template:
<text v-if="debugMode" x="10" y="290" font-size="10" fill="red">
DEBUG: {{ JSON.stringify(V) }}
</text>Using Without Vue
The tooling is set up primarily to work with Vue, but it is not required. Visualizations may be created with any framework (or none at all) as long as they:
- Include an
enms-plugin-manifest.jsonin the package root. - Use the
@enms-vis/enms-web-pluginlibrary to communicate with the host.
Example using plain TypeScript (no Vue):
import { WebPluginHost } from '@enms-vis/enms-web-plugin'
const host = new WebPluginHost();
host.onInputUpdate((payload) => {
// Update DOM directly
const el = document.getElementById(payload.inputId);
if (el && payload.type === 'param') {
el.textContent = String(payload.currentValue?.valueEnd ?? '–');
}
});
host.onConfigUpdate((config) => {
console.log('Debug mode:', config.debugMode);
});
host.connect();The @enms-vis/enms-web-plugin-vue package is a convenience wrapper – it is not loaded or required by enms-web at runtime. Only enms-plugin-manifest.json and the built output matter.
Manifest Format (enms-plugin-manifest.json)
The manifest file is the contract between a visualization plugin and enms-web. It tells enms-web what data the plugin needs so it can build the configuration UI and deliver the correct input payloads at runtime.
Top-level structure
{
"indexPath": "dist/index.html",
"inputs": [ ... ]
}| Field | Required | Description |
|---|---|---|
| indexPath | No | Path to the plugin's entry HTML file relative to the package root. Defaults to dist/index.html. |
| inputs | Yes | Array of input definitions. Each input has a unique inputId that becomes a key in the reactive V state. |
Input types
Every input object has a type that determines what data it represents and how enms-web resolves it.
device – Device input
Provides a DeviceInputPayload containing device metadata and (optionally) its parameters.
{
"inputId": "boiler_1",
"type": "device",
"required": true,
"inputName": { "en": "Main boiler", "pl": "Kocioł główny" },
"description": { "en": "The boiler whose data will be displayed." },
"valueSpec": {
"type": "device",
"params": [ ... ]
}
}| Field | Description |
|---|---|
| inputId | Unique key – becomes V.boiler_1 in the plugin. |
| required | If true, the user must assign a device. The plugin should still handle missing values gracefully. |
| inputName | Localized human-readable label shown in the configuration UI (MlText – object with locale keys). |
| description | Localized description shown in the configuration UI. |
| deviceDsl | Optional default DSL expression to pre-select a device. |
| multiple | If true, the user can assign multiple devices; the payload becomes an array. |
| valueSpec.params | Array of parameter definitions to load for the device (see below). |
| valueSpec.deviceExtraFields | Optional list of extra device fields to include (e.g. "serialNumber", "notes", "deviceCfg"). |
Device parameters: deviceParams vs param
Inside valueSpec.params you can use two kinds of entries:
deviceParams – matches multiple parameters by DSL in the context of the device. Each matched parameter is automatically keyed by its parameter code. This is the most common and concise approach:
{
"type": "deviceParams",
"paramDsl": "param_code = current_temperature_setpoint,temperature_actual,relay_state",
"valueSpec": {
"type": "param",
"currentValue": { "value": true }
}
}param – declares a single named parameter with its own inputId. Use this when you need a specific inputId that differs from the parameter code, or when each parameter requires a different valueSpec:
{
"inputId": "active_power",
"type": "param",
"paramDsl": "param_code = 'active_power'",
"valueSpec": {
"type": "param",
"currentValue": { "value": true, "valueUnit": "W" }
}
}currentValue field spec (AggFieldSpec)
Controls which measurement fields are included in ParamInputPayload.currentValue:
| Field | Type | Description |
|---|---|---|
| value | boolean | Include value fields: valueEnd, valueBegin, avg, min, max, stddev, increase, decrease, delta. |
| valueUnit | string | Target unit for value fields (e.g. "kWh"). If not set, the parameter's native unit is used. |
| derivative | boolean | Include derivative fields: derivativeMin, derivativeMax, derivativeAvg, derivativeStddev. |
| derivativeUnit | string | Target unit for derivative fields. |
| integral | boolean | Include integral field. |
| integralUnit | string | Target unit for integral. |
| stats | boolean | Include statistics: count, origin, quality, avgIntegrity, deltaIntegrity. |
| formatTs | boolean | Include human-readable timestamps: formattedTs, formattedTsEnd, formattedTsRange, formattedModifiedTs. |
param – Standalone parameter input
Provides a ParamInputPayload for a parameter that is not scoped to a specific device input.
{
"inputId": "outdoor_temp",
"type": "param",
"paramDsl": "param_code = 'outdoor_temperature'",
"valueSpec": {
"type": "param",
"currentValue": { "value": true }
}
}number, text, boolean – Primitive inputs
Primitive inputs let the user configure the plugin's behaviour (e.g. thresholds, labels, feature flags). They do not reference any device or parameter.
{ "inputId": "temp_threshold", "type": "number", "defaultValue": 22.0,
"inputName": { "en": "Temperature threshold" } }
{ "inputId": "title_label", "type": "text", "defaultValue": "Heating overview",
"inputName": { "en": "Widget title" } }
{ "inputId": "show_labels", "type": "boolean", "defaultValue": true,
"inputName": { "en": "Show labels" } }Access in the template:
<text v-if="V?.show_labels?.value">{{ V?.title_label?.value ?? '' }}</text>Full example
{
"inputs": [
{
"inputId": "ekostat_1",
"type": "device",
"inputName": { "pl": "Urządzenie ekostat" },
"valueSpec": {
"type": "device",
"params": [
{
"type": "deviceParams",
"paramDsl": "param_code = current_temperature_setpoint,temperature_actual,relay_state",
"valueSpec": {
"type": "param",
"currentValue": {
"value": true
}
}
}
]
}
},
{
"inputId": "temp_threshold",
"type": "number",
"defaultValue": 22.0,
"inputName": { "en": "Temperature threshold", "pl": "Próg temperatury" }
}
]
}Building for enms-web
When a visualization plugin is deployed to enms-web, the following structure is expected:
my-visualization/
├── enms-plugin-manifest.json ← plugin input definitions (required)
└── dist/ ← built output directory (default)
├── index.html ← entry point loaded in the iframe
├── assets/
│ ├── index-XXXXX.js ← bundled application code
│ └── index-XXXXX.css ← bundled styles
└── …This layout is fully compatible with the default Vite build output – npm run build produces the dist/ directory with index.html and hashed assets out of the box. The only addition on top of a standard Vite project is the enms-plugin-manifest.json file in the package root.
enms-plugin-manifest.json
The manifest is the contract between the visualization and enms-web. It declares what inputs the plugin needs. enms-web reads it to build the widget configuration UI and to know what data to deliver. See Manifest Format for the full specification.
Key fields:
inputs– array of input definitions. Each input has aninputId, atype(device,param,number,text,boolean), and an optionalvalueSpecdescribing what data fields to include.indexPath(optional) – path to the entry HTML file relative to the package root. Defaults todist/index.htmlif not specified.
dist directory
The dist/ directory is produced by npm run build (Vite build). It contains the compiled index.html and all bundled assets. enms-web serves the contents of this directory inside a sandboxed iframe.
index.html
The entry point loaded by enms-web inside the iframe. It must mount the application and establish the plugin connection (handled automatically when using the Vue scaffolding).
Package files
The package.json files field controls what gets published:
{
"files": [
"dist/**/*",
"enms-plugin-manifest.json"
]
}This ensures only the built output and the manifest are included in the deployable package.
