Skip to main content
Version: 1.0

Virtual Components

The Virtual Components API allows scripts to interact with virtual components configured on the device. Virtual components are generic entities (numbers, text, booleans, buttons, etc.) that appear in the device's web interface and can be controlled via the standard RPC API. Scripts can also declare the virtual components they need directly in their code instead of creating them via RPC by hand — see Managed Virtual Components.

Overview​

Virtual components provide a way to:

  • Access and control virtual components from scripts
  • Store and manage state persistently
  • Respond to user input from the device UI
  • Integrate with home automation systems
  • Implement custom logic based on virtual component values

Supported Component Types​

  • Number - Numeric values with min/max limits
  • Text - String values
  • Enum - Selection from predefined options
  • Boolean - True/false switches
  • Button - Push buttons with various press types
  • Group - Organizational containers

Quick Example​

This example declares its virtual components in @meta instead of assuming they were already created via RPC — see Managed Virtual Components.

/* @meta {
"vc": {
"temperature": {
"type": "number",
"config": {
"name": "Room Temperature",
"persisted": true,
"min": -40,
"max": 85,
"default_value": 20,
"meta": {"ui": {"unit": "°C", "step": 0.1}}
}
},
"status": {"type": "text", "config": {"name": "Status"}},
"alarm": {"type": "boolean", "config": {"name": "High Temp Alarm"}}
}
} */

// Get handles to the declared virtual components
let temperature = Script.getVcHandle("temperature");
let status = Script.getVcHandle("status");
let alarm = Script.getVcHandle("alarm");

// Set values
temperature.setValue(23.5);
status.setValue("Normal");
alarm.setValue(false);

// Read values
console.log("Temperature:", temperature.getValue());

// Listen for changes
temperature.on("change", function(event) {
console.log("Temperature changed to", event.value);
if (event.value > 30) {
alarm.setValue(true);
status.setValue("High temperature alert!");
}
});

API Reference​

Virtual Global Object​

The Virtual object provides access to virtual component functionality.

Virtual.getHandle(key)​

Obtains a handle to a virtual component instance by its component key.

Virtual.getHandle(key) -> Virtual instance or null

Parameters​

  • key (string) - Component identifier in format "type:id" where:
    • type is the component type (number, text, boolean, enum, button, group)
    • id is the numeric component ID (typically 200+)

Return Value​

Returns a Virtual instance on success, or null if the component doesn't exist or on error.

Example​

let mySwitch = Virtual.getHandle("boolean:200");
let myButton = Virtual.getHandle("button:201");
let myNumber = Virtual.getHandle("number:202");

Script.getVcHandle(role)​

Obtains a handle to a managed virtual component by its role name. The returned handle is the same Virtual instance as from Virtual.getHandle().

Script.getVcHandle(role) -> Virtual instance or null

Parameters​

  • role (string) - The role name from the script's @meta vc declaration

Return Value​

Returns a Virtual instance on success, or null if the role is not found or the script has no managed VCs.

Example​

// @meta {"vc":{"temp":{"type":"number","config":{"name":"Temperature"}}}}
let temp = Script.getVcHandle("temp");
temp.setValue(22.5);

Virtual Instance Methods​

setValue(new_value)​

Sets the value of the virtual component. The value is applied asynchronously when the script execution context exits.

instance.setValue(new_value) -> undefined

Note: Not supported for Button and Group components. Attempting to use on these types will raise an exception.

Parameters​

  • new_value - The value to set (type depends on component type)

Examples​

let temperature = Virtual.getHandle("number:200");
let deviceName = Virtual.getHandle("text:200");
let isEnabled = Virtual.getHandle("boolean:200");

temperature.setValue(25.5);
deviceName.setValue("Living Room Sensor");
isEnabled.setValue(true);

getValue()​

Returns the current value of the virtual component.

instance.getValue() -> value or undefined

Note: Not supported for Button and Group components.

Return Value​

Returns the component's value, or undefined on error.

Example​

let temperature = Virtual.getHandle("number:200");
temperature.setValue(22.5);
console.log("Current temperature:", temperature.getValue()); // 22.5

getStatus()​

Returns the status of the virtual component.

instance.getStatus() -> object or undefined

Return Value​

Returns the component's status object, or undefined if the component no longer exists. The status format varies by component type.

See the status documentation for each component type:

Example​

let sensor = Virtual.getHandle("number:200");
let status = sensor.getStatus();
console.log("Status:", JSON.stringify(status));
// Example output: {"value":42,"source":"script:1","last_update_ts":1753928721}

getConfig()​

Returns the configuration of the virtual component.

instance.getConfig() -> object or undefined

Return Value​

Returns the component's configuration object, or undefined if the component no longer exists. The configuration format varies by component type.

See the configuration documentation for each component type:

Example​

let temp = Virtual.getHandle("number:200");
let config = temp.getConfig();
console.log("Config:", JSON.stringify(config));

setConfig(config_obj)​

Updates the configuration of the virtual component.

instance.setConfig(config_obj) -> undefined

Parameters​

  • config_obj (object) - Configuration properties to update. The available properties vary by component type.

See the configuration documentation for each component type:

Example​

let temperature = Virtual.getHandle("number:200");

temperature.setConfig({
name: "Room Temperature",
persisted: true,
min: -50,
max: 100,
default_value: 20,
meta: {
ui: {
unit: "°C",
step: 0.1
}
}
});

on(event, callback)​

Attaches an event handler to the virtual component.

instance.on(event, callback) -> listener_id

Parameters​

  • event (string) - Event name to listen for
  • callback (function) - Function to call when event occurs
    • Receives event info object with:
      • source - Source that triggered the event
      • value - New value (for change events)

Return Value​

Returns listener ID that can be used with off() to remove the listener.

Supported Events​

For Number, Text, Boolean, Enum components:

  • change - Triggered when value changes

For Button components:

  • single_push - Single press
  • double_push - Double press
  • triple_push - Triple press
  • long_push - Long press

Examples​

// Value change listener
let temperature = Virtual.getHandle("number:200");
temperature.on("change", function(event) {
console.log("Temperature changed to", event.value);
console.log("Changed by", event.source);
});

// Button press listeners
let button = Virtual.getHandle("button:201");

button.on("single_push", function(event) {
console.log("Button pressed once");
});

button.on("double_push", function(event) {
console.log("Button pressed twice");
});

button.on("long_push", function(event) {
console.log("Button long pressed");
});

off(listener_id)​

Removes an event listener.

instance.off(listener_id) -> undefined

Parameters​

  • listener_id - The ID returned by on()

Example​

let sensor = Virtual.getHandle("number:200");

// Add listener
let listenerId = sensor.on("change", function(event) {
console.log("Value changed to", event.value);
});

// Later, remove the listener
sensor.off(listenerId);

Complete Examples​

These examples declare their virtual components in @meta (see Managed Virtual Components) instead of assuming they were pre-created via RPC. This avoids hard-coding a numeric ID that depends on creation order and on what else has been created on the device: Script.getVcHandle(role) always resolves to the right instance.

Temperature Monitor with Alarm​

/* @meta {
"vc": {
"currentTemp": {
"type": "number",
"config": {
"name": "Temperature Sensor",
"persisted": true,
"min": -50,
"max": 150,
"default_value": 20,
"meta": {"ui": {"unit": "°C", "step": 0.1}}
}
},
"tempStatus": {
"type": "text",
"config": {"name": "Status", "persisted": false, "default_value": "Normal"}
},
"highTempAlarm": {
"type": "boolean",
"config": {"name": "High Temp Alarm"}
},
"resetButton": {
"type": "button",
"config": {"name": "Reset"}
}
}
} */

let currentTemp = Script.getVcHandle("currentTemp");
let tempStatus = Script.getVcHandle("tempStatus");
let highTempAlarm = Script.getVcHandle("highTempAlarm");
let resetButton = Script.getVcHandle("resetButton");

// Temperature threshold
const TEMP_THRESHOLD = 35;

// Monitor temperature changes
currentTemp.on("change", function(event) {
console.log("Temperature:", event.value, "°C");

if (event.value > TEMP_THRESHOLD) {
highTempAlarm.setValue(true);
tempStatus.setValue("HIGH TEMPERATURE: " + event.value + "°C");

// Send alert
Shelly.call("HTTP.Request", {
method: "GET",
url: "http://alert.server/high-temp?value=" + event.value
});
} else {
highTempAlarm.setValue(false);
tempStatus.setValue("Normal: " + event.value + "°C");
}
});

// Reset button
resetButton.on("single_push", function() {
console.log("Reset pressed");
highTempAlarm.setValue(false);
tempStatus.setValue("System reset");
});

// Simulate temperature readings (in real use, read from actual sensor)
Timer.set(5000, true, function() {
let mockTemp = 20 + Math.random() * 30;
currentTemp.setValue(mockTemp);
});

console.log("Temperature monitor started");

State Machine with Enum​

/* @meta {
"vc": {
"machineState": {
"type": "enum",
"config": {
"name": "Machine State",
"persisted": true,
"options": ["idle", "running", "paused", "error"],
"default_value": "idle"
}
},
"startButton": {"type": "button", "config": {"name": "Start"}},
"stopButton": {"type": "button", "config": {"name": "Stop"}},
"statusText": {"type": "text", "config": {"name": "Status"}}
}
} */

let machineState = Script.getVcHandle("machineState");
let startButton = Script.getVcHandle("startButton");
let stopButton = Script.getVcHandle("stopButton");
let statusText = Script.getVcHandle("statusText");

// State change handler
machineState.on("change", function(event) {
console.log("State changed to:", event.value);
statusText.setValue("Machine is " + event.value);

// Take action based on state
switch(event.value) {
case "running":
Shelly.call("Switch.Set", {id: 0, on: true});
break;
case "idle":
case "paused":
Shelly.call("Switch.Set", {id: 0, on: false});
break;
case "error":
// Flash LED or send notification
console.log("ERROR STATE - Intervention required");
break;
}
});

// Control buttons
startButton.on("single_push", function() {
let current = machineState.getValue();
if (current === "idle" || current === "paused") {
machineState.setValue("running");
}
});

stopButton.on("single_push", function() {
machineState.setValue("idle");
});

// Error detection (example)
Timer.set(10000, true, function() {
if (machineState.getValue() === "running") {
// Check for error condition
let systemStatus = Shelly.getComponentStatus("sys");
if (systemStatus.ram_free < 10000) {
machineState.setValue("error");
}
}
});

Data Logger with Persistence​

/* @meta {
"vc": {
"sampleCount": {
"type": "number",
"config": {"persisted": true, "default_value": 0}
},
"lastSample": {
"type": "number",
"config": {"persisted": true, "meta": {"ui": {"unit": "units"}}}
},
"averageValue": {
"type": "number",
"config": {"persisted": true, "meta": {"ui": {"unit": "units"}}}
},
"resetButton": {
"type": "button",
"config": {"name": "Reset"}
}
}
} */

let sampleCount = Script.getVcHandle("sampleCount");
let lastSample = Script.getVcHandle("lastSample");
let averageValue = Script.getVcHandle("averageValue");
let resetButton = Script.getVcHandle("resetButton");

// Running totals
let runningTotal = 0;
let count = sampleCount.getValue() || 0;

// If we have previous data, calculate running total
if (count > 0) {
runningTotal = (averageValue.getValue() || 0) * count;
}

// Collect data every minute
Timer.set(60000, true, function() {
// Get sensor reading (example)
let reading = Math.random() * 100; // Replace with actual sensor

// Update statistics
count++;
runningTotal += reading;
let avg = runningTotal / count;

// Update virtual components
sampleCount.setValue(count);
lastSample.setValue(reading);
averageValue.setValue(avg);

console.log("Sample", count, ":", reading, "Avg:", avg);
});

// Reset statistics
resetButton.on("single_push", function() {
console.log("Resetting statistics");
count = 0;
runningTotal = 0;

sampleCount.setValue(0);
lastSample.setValue(0);
averageValue.setValue(0);
});

console.log("Data logger started. Samples:", count);

Best Practices​

1. Check Component Existence​

let component = Virtual.getHandle("number:200");
if (!component) {
console.log("Virtual component not found!");
return;
}

2. Use Appropriate Component Types​

  • Use Number for numeric values with ranges
  • Use Boolean for on/off states
  • Use Enum for multiple choice options
  • Use Button for user actions
  • Use Text for display strings

3. Configure Persistence Appropriately​

// Persist important state
stateComponent.setConfig({ persisted: true });

// Don't persist temporary values
tempDisplay.setConfig({ persisted: false });

4. Handle Events Efficiently​

// Good - Single handler for multiple actions
button.on("single_push", handleButtonPress);
button.on("double_push", handleButtonPress);

function handleButtonPress(event) {
// Handle different press types
}

// Avoid creating many anonymous functions

5. Clean Up Event Listeners​

let listeners = [];

// Track listeners
listeners.push(component.on("change", handler));

// Clean up when done
for (let id of listeners) {
component.off(id);
}

Limitations​

  • Not all component types support all methods
  • Values are applied asynchronously
  • Limited number of virtual components per device

Creating Virtual Components​

Manual Creation via RPC​

Virtual components can be created through the device's RPC API and then accessed by scripts using the returned ID:

# Create a virtual number component
curl -X POST -d '{
"method": "Virtual.Add",
"params": {
"type": "number",
"config": {
"name": "Temperature",
"min": -50,
"max": 100
}
}
}' http://<device_ip>/rpc

# Returns: {"id": 200}

Once created, scripts can access them using the returned ID:

let temp = Virtual.getHandle("number:200");

Managed Virtual Components​

Since version 2.0.0

Scripts can declare virtual components directly in their @meta header. Declared components are created automatically when the script starts, updated if the declaration changes, and removed when the script is deleted. There is no need to create them manually via RPC.

Declaration Format​

Add a vc object to the script's @meta comment. Each key is a role name (chosen by you), and the value describes the component:

// @meta {"vc":{"role_name":{"type":"<type>","config":{...}}}}

Multi-line block comments are also supported:

/* @meta {
"vc": {
"role_name": {"type": "<type>", "config": {...}},
"other_role": {"type": "<type>", "config": {...}}
}
} */
note

The @meta comment must be the first non-empty line of the script. Multi-line metadata must use a single /* ... */ block comment — per-line // comments are not supported for multi-line metadata.

PropertyTypeRequiredDescription
typestringyesComponent type: number, text, boolean, enum, button, group
configobjectyesInitial configuration (same properties as setConfig())
accessstringnoPermissions granted to other clients, as a string of letters (see below). Defaults to every permission except manage if omitted. The owning script always keeps full control; manage (delete) can never be granted to anyone else
membersarray of stringsnoGroup membership, only valid when type is group. See Group Membership

access letters:

LetterGrants
*Everything (overrides any other letters present)
rRead (GetStatus)
wWrite (Set) — implies r
cGet config (GetConfig)
CSet config (SetConfig) — implies c. Case-sensitive: c and C are different permissions
mManage (delete). Always stripped from managed VCs, regardless of what is requested here

Unrecognized letters are silently ignored rather than rejected, so a typo (wrong case, stray character) quietly grants fewer permissions than intended instead of failing the declaration. "r" grants read-only; omitting access grants everything but manage.

Script.getVcHandle(role)​

Returns a Virtual handle for a managed component by its role name instead of its numeric ID. The handle supports the same methods as Virtual.getHandle() (setValue, getValue, getStatus, getConfig, setConfig, on, off).

Script.getVcHandle(role) -> Virtual instance or null

Parameters:

  • role (string) - The role name from the @meta vc declaration

Return value: A Virtual instance, or null if the role is not found.

Example​

// @meta {"vc":{"temp":{"type":"number","config":{"name":"Temperature","min":-40,"max":85,"meta":{"ui":{"unit":"°C"}}}},"alarm":{"type":"boolean","config":{"name":"High Temp Alarm"}}}}

let temp = Script.getVcHandle("temp");
let alarm = Script.getVcHandle("alarm");

Timer.set(10000, true, function() {
let s = Shelly.getComponentStatus("temperature", 0);
if (!s) return;
temp.setValue(s.tC);
alarm.setValue(s.tC > 35);
});

Group Membership​

Since version 2.2.0

A group role can declare its own membership with members instead of being curated by hand through Group.Set. Each entry is a reference to another role declared in the same @meta block, written as "@role". Role references exist because the keys of same-manifest components are not known until they are created — the manifest can only name them by role.

/* @meta {
"vc": {
"heating": {"type": "boolean", "config": {"name": "Heating"}},
"level": {"type": "number", "config": {"name": "Level"}},
"room": {"type": "group", "config": {"name": "Room"},
"members": ["@heating", "@level"]}
}
} */

room's status then reflects the resolved members:

{"value": ["boolean:200", "number:200"], "source": "sys"}

Notes:

  • Group-only: members on any other type is rejected.
  • Omit to leave it alone: a declaration without members does not touch the group's current value, so a manifest-declared group can still be curated by the user via Group.Set.
  • Re-applied every start: if members is present, it is re-applied on every reconcile, so the declaration always wins over prior Group.Set edits. To let user edits stick, drop members from the declaration.
  • Strict resolution: an "@role" that does not match another role in the manifest rejects the whole declaration — see Lifecycle.

Lifecycle​

  • Script start: The vc section is parsed and reconciled. New components are created, existing ones are updated if the declaration changed, and components whose role was removed from the declaration are deleted.
  • Script deletion: All virtual components owned by the script are removed.
  • Invalid or unsatisfiable declarations: the whole vc section is validated — JSON errors, unknown types, bad members references, and running over the device's virtual component budget — before anything is created or removed. If any of it fails, the previous set of managed components is left exactly as it was and the script does not start; Script.Start returns the failure (RPC error -110) with a message identifying the role and the problem, e.g. vc: room: no such role 'nope'.
  • Over the virtual component budget: the device has a device-wide limit on virtual components, shared by all scripts, services, and manually-created ones. A declaration that needs to create new managed components beyond that limit fails to start the script, for the same reason as any other unsatisfiable declaration. A declaration that creates nothing new — every role already exists with the same type — always succeeds, even if the device is already over the limit (e.g. after a firmware update that lowered it); a script does not get refused for VCs it already had.
note

Component IDs are assigned automatically and may change if the component is recreated. Always use Script.getVcHandle(role) to access managed components — never hard-code the numeric ID.

Multiple Components​

A script can declare as many components as needed:

/* @meta {
"vc": {
"setpoint": {
"type": "number",
"config": {"name": "Setpoint", "min": 10, "max": 30,
"default_value": 20, "persisted": true}
},
"mode": {
"type": "enum",
"config": {"name": "Mode", "options": ["off", "heat", "cool"],
"default_value": "off", "persisted": true}
},
"status": {
"type": "text",
"config": {"name": "Status"}
}
}
} */

let setpoint = Script.getVcHandle("setpoint");
let mode = Script.getVcHandle("mode");
let status = Script.getVcHandle("status");

mode.on("change", function(ev) {
status.setValue("Mode changed to " + ev.value);
});
  • Shelly API - Core device control
  • Timer API - Scheduling and delays
  • Virtual Components Service - RPC API documentation