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:typeis the component type (number, text, boolean, enum, button, group)idis 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@metavcdeclaration
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:
- Number configuration
- Text configuration
- Boolean configuration
- Enum configuration
- Button configuration
- Group configuration
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:
- Number configuration
- Text configuration
- Boolean configuration
- Enum configuration
- Button configuration
- Group configuration
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)
- Receives event info object with:
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 pressdouble_push- Double presstriple_push- Triple presslong_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
Numberfor numeric values with ranges - Use
Booleanfor on/off states - Use
Enumfor multiple choice options - Use
Buttonfor user actions - Use
Textfor 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": {...}}
}
} */
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.
| Property | Type | Required | Description |
|---|---|---|---|
type | string | yes | Component type: number, text, boolean, enum, button, group |
config | object | yes | Initial configuration (same properties as setConfig()) |
access | string | no | Permissions 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 |
members | array of strings | no | Group membership, only valid when type is group. See Group Membership |
access letters:
| Letter | Grants |
|---|---|
* | Everything (overrides any other letters present) |
r | Read (GetStatus) |
w | Write (Set) — implies r |
c | Get config (GetConfig) |
C | Set config (SetConfig) — implies c. Case-sensitive: c and C are different permissions |
m | Manage (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@metavcdeclaration
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:
memberson any other type is rejected. - Omit to leave it alone: a declaration without
membersdoes not touch the group's current value, so a manifest-declared group can still be curated by the user viaGroup.Set. - Re-applied every start: if
membersis present, it is re-applied on every reconcile, so the declaration always wins over priorGroup.Setedits. To let user edits stick, dropmembersfrom 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
vcsection 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
vcsection is validated — JSON errors, unknown types, badmembersreferences, 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.Startreturns 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.
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);
});
Related Topics
- Shelly API - Core device control
- Timer API - Scheduling and delays
- Virtual Components Service - RPC API documentation