Philips Hue in Shelly App
- Home
- Scripting & Virtual components
- Philips Hue in Shelly App
Control Hue lights and get motion status from Hue Motion Sensors via the Hue Bridge (V2).
It is possible to add and control Philips Hue lights (on/off) directly from the Shelly app using scripting and virtual components.
– Now also added a script to retrieve status for Hue Motions Sensor every 1 sec.
Requirements & Notes
- Tested with Hue Bridge V2
- The Hue Bridge IP address must be known and set as static
- Requires a Shelly device that supports scripting and virtual components (Gen3+)
- One-way communication only
- Shelly cannot detect if a Hue light is turned on or off externally (for example from the Hue app or a wall switch).
- Shelly sends ON/OFF commands and assumes the light state, without receiving any feedback.
- Depending on the number of Hue devices, a Shelly Premium subscription may be required.
- Supports:
- On/off control
- Scene control
- Group control
This enables local control of individual Hue lights, groups, and scenes directly from Shelly devices and virtual components.
Step 1 – Create a user on the Hue Bridge
To get started, you must create an authorized user on the Hue Bridge.
Philips provides a well-written guide explaining this process:
https://developers.meethue.com/develop/get-started-2/
Step 2 – Find IDs for lights, groups, and scenes
Find Light IDs
You can retrieve information about all lights connected to the Hue Bridge using the built-in Hue Bridge Debug Tool, or by opening the following URL in your browser:
https://<bridge ip address>/api/<generated username>/lights
Example
http://192.168.1.10/api/1028d66426293e821ecfd9ef1a0731df/lights

Find Group IDs
https://<bridge ip address>/api/<generated username>/groups
Example
http://192.168.1.10/api/1028d66426293e821ecfd9ef1a0731df/groups

Find Scene ID and Group ID
To control a scene, both the Scene ID and the Group ID are required.
https://<bridge ip address>/api/<generated username>/scenes
Example
http://192.168.1.10/api/1028d66426293e821ecfd9ef1a0731df/scenes

Find Sensor ID
https://<bridge ip address>/api/<generated username>/sensors
Example
http://192.168.1.10/api/1028d66426293e821ecfd9ef1a0731df/sensors

Step 3 – The Scripts
Below is a collection of scripts, each providing different functionality and use cases.
Common to all scripts:
- You must enter the Hue Bridge IP address
- You must enter the authorized username
The first scripts are designed to create Hue lights as virtual devices in the Shelly app.
This allows direct control and use in Shelly scenes, just like native Shelly devices.
You create the virtual components manually on the Shelly device.
Each Shelly device can have up to 10 BTHome components, virtual devices, and groups combined.
Each script includes a short description explaining:
- What it does
- Which virtual components must be created
Available Scripts
*Script* - Add and control up to 9 Hue lights (On/Off)
In the Shelly:
- Create one Boolean virtual component per light (max 9)
In the Script, type in/replace:
- Hue bridge IP adresss (HUE_URL)
- Username achieved from HUE Bridge (HUE_USERNAME)
- Enter the corresponding Light ID, VC ID, etc. (Under DEVICES =)
// Controls up to 9 Hue LIGHTS via Virtual Booleans ONLY
let HUE_URL = "http://192.168.2.52/"; // must end with /
let HUE_USERNAME = "MSzjJR7-HxXDMbKIdEJ7MR0JAqc5UuHmdk-oy6ts";
// ====================LIGHT CONFIG (max 9) ====================
//
// Each entry below defines ONE Hue light that can be triggered from ONE
// Virtual Boolean in the Shelly UI.
//
// Fields:
//
// Id : string -> Hue Light ID
// name : string -> your label (log-only, does not affect Hue)
// boolean_id : number -> Virtual Button ID in Shelly (e.g. 200..208)
//
//
// ===============================================================
let DEVICES = [
{ id: "1", name: "kitchen", boolean_id: 200 },
{ id: "2", name: "hallway", boolean_id: 201 },
{ id: "3", name: "livingroom", boolean_id: 202 },
// Remove the leading `//` from each line you want to use below
//{ id: "4", name: "bath", boolean_id: 203 },
//{ id: "5", name: "bath", boolean_id: 204 },
//{ id: "6", name: "bath", boolean_id: 205 },
//{ id: "7", name: "bath", boolean_id: 206 },
//{ id: "8", name: "bath", boolean_id: 207 },
//{ id: "9", name: "bath", boolean_id: 208 },
];
let BOOT_SYNC_DELAY_MS = 350;
let BOOLEAN_SELF_UPDATE_GUARD_MS = 300;
function log(msg) {
print("[HUE]", msg);
}
// ---- Per-boolean loop protection ----
let _ignoreNextBooleanStatus = {};
function guardBoolean(booleanId) {
_ignoreNextBooleanStatus[booleanId] = true;
Timer.set(BOOLEAN_SELF_UPDATE_GUARD_MS, false, function () {
_ignoreNextBooleanStatus[booleanId] = false;
});
}
function setBoolean(booleanId, value) {
try {
guardBoolean(booleanId);
Shelly.call("Boolean.Set", { id: booleanId, value: !!value });
log("Boolean.Set id=" + booleanId + " → " + (!!value));
} catch (e) {
log("ERROR Boolean.Set (" + booleanId + "): " + e);
}
}
// ---- HTTP wrapper ----
function request(params, callback, userdata) {
try {
log("HTTP " + params.method + " → " + params.url);
if (params.body) log("HTTP body: " + params.body);
Shelly.call(
"HTTP.Request",
params,
function (result, error_code, error_message, userdata) {
try {
if (error_code) {
log("HTTP ERROR " + error_code + ": " + error_message);
return;
}
log("HTTP OK " + result.code);
if (result.body) log("HTTP response: " + result.body);
if (userdata && userdata.callback) {
userdata.callback(result, userdata.userdata);
}
} catch (e) {
log("ERROR HTTP callback: " + e);
}
},
{ callback: callback, userdata: userdata }
);
} catch (e) {
log("ERROR HTTP.Request: " + e);
}
}
function hueLightUrl(lightId) {
return HUE_URL + "api/" + HUE_USERNAME + "/lights/" + lightId;
}
function toBool(v) {
return v === true || v === "true" || v === 1 || v === "1" || v === "on";
}
// ---- Hue read (sync) ----
function get_state(dev, callback) {
request(
{ method: "GET", url: hueLightUrl(dev.id) },
function (result, callback) {
try {
let data = JSON.parse(result.body);
let isOn = !!(data.state && data.state.on);
log("Hue state (" + dev.name + ") = " + isOn);
setBoolean(dev.boolean_id, isOn);
if (callback) callback(isOn);
} catch (e) {
log("ERROR parsing Hue state (" + dev.name + "): " + e);
}
},
callback
);
}
// ---- Hue write ----
function put_onoff(dev, state) {
try {
state = !!state;
setBoolean(dev.boolean_id, state);
log("Setting Hue LIGHT (" + dev.name + ") to " + (state ? "ON" : "OFF"));
request({
method: "PUT",
url: hueLightUrl(dev.id) + "/state",
body: state ? '{"on":true}' : '{"on":false}',
});
} catch (e) {
log("ERROR put_onoff (" + dev.name + "): " + e);
}
}
// ---- Find device by boolean component ----
function getDeviceByComponent(componentStr) {
for (let i = 0; i < DEVICES.length; i++) {
if (componentStr === "boolean:" + DEVICES[i].boolean_id) {
return DEVICES[i];
}
}
return null;
}
// ---- Virtual Boolean handler ----
Shelly.addStatusHandler(function (status) {
try {
if (!status || !status.component) return;
if (!status.delta || typeof status.delta.value === "undefined") return;
if (status.component.indexOf("boolean:") !== 0) return;
let dev = getDeviceByComponent(status.component);
if (!dev) return;
if (_ignoreNextBooleanStatus[dev.boolean_id]) {
log("Boolean ignored (self-update): " + dev.name);
return;
}
let desired = toBool(status.delta.value);
log("Boolean change → " + dev.name + " = " + desired);
put_onoff(dev, desired);
} catch (e) {
log("ERROR status handler: " + e);
}
});
// ---- Startup ----
log("Started. UI-only Hue multi-light (try/catch enabled)");
log("Devices=" + DEVICES.length);
// ---- Boot sync ----
Timer.set(800, false, function () {
log("Boot sync → reading Hue state...");
for (let i = 0; i < DEVICES.length; i++) {
(function (dev, idx) {
Timer.set(idx * BOOT_SYNC_DELAY_MS, false, function () {
try {
get_state(dev, function () {});
} catch (e) {
log("ERROR boot sync (" + dev.name + "): " + e);
}
});
})(DEVICES[i], i);
}
});
*Script* - Add and activate up to 9 Hue scenes
In the Shelly:
- Create one Button virtual component per scene (max 9)
In the Script, type in/replace:
- Hue bridge IP adresss (HUE_URL)
- Username achieved from HUE Bridge (HUE_USERNAME)
- Enter both the corresponding Scene ID and Group ID, VC ID, etc. (Under SCENES =)
// Shelly (Gen2) -> Philips Hue Bridge (local, v1 API)
// Controls up to 9 Hue SCENES via separate Virtual BUTTONS (UI only)
// Virtual button emits EVENT: name=button, component=button:<id>, info.event=single_push
//
// Hue v1 NOTE:
// A scene is recalled via a GROUP endpoint (room/zone):
// PUT /api/<user>/groups/<groupId>/action { "scene": "<sceneId>" }
//
// Hardened with try/catch
let HUE_URL = "http://192.168.2.52/"; // must end with /
let HUE_USERNAME = "MSzjJR7-HxXDMbKIdEJ7MR0JAqc5UuHmdk-oy6ts";
// ==================== SCENE CONFIG (max 9) ====================
//
// Each entry below defines ONE Hue scene that can be triggered from ONE
// Virtual Button in the Shelly UI.
//
// Fields:
// enabled : true/false -> whether this entry is active (false = ignored)
// button_id : number -> Virtual Button ID in Shelly (e.g. 200..208)
// name : string -> your label (log-only, does not affect Hue)
// groupId : string -> Hue group/room/zone ID (required by Hue v1)
// sceneId : string -> Hue scene ID (from /api/<user>/scenes)
//
// Example:
// { enabled:true, button_id:200, name:"Living - Cozy", groupId:"3", sceneId:"abcd..." }
//
// ===============================================================
let SCENES = [
{ enabled: true, button_id: 200, name: "Living - Cozy", groupId: "13", sceneId: "UbayCv-dHbdoLAHE" },
{ enabled: false, button_id: 201, name: "Living - TV", groupId: "3", sceneId: "FVEDRIgsEIPmixn" },
{ enabled: false, button_id: 202, name: "Kitchen - Bright", groupId: "1", sceneId: "u0L4rnehnkUEau3n" },
{ enabled: false, button_id: 203, name: "Scene 4", groupId: "0", sceneId: "" },
{ enabled: false, button_id: 204, name: "Scene 5", groupId: "0", sceneId: "" },
{ enabled: false, button_id: 205, name: "Scene 6", groupId: "0", sceneId: "" },
{ enabled: false, button_id: 206, name: "Scene 7", groupId: "0", sceneId: "" },
{ enabled: false, button_id: 207, name: "Scene 8", groupId: "0", sceneId: "" },
{ enabled: false, button_id: 208, name: "Scene 9", groupId: "0", sceneId: "" },
];
function log(msg) {
print("[HUE]", msg);
}
// ---- HTTP wrapper ----
function request(params) {
try {
log("HTTP " + params.method + " -> " + params.url);
if (params.body) log("HTTP body: " + params.body);
Shelly.call("HTTP.Request", params, function (result, error_code, error_message) {
try {
if (error_code) {
log("HTTP ERROR " + error_code + ": " + error_message);
return;
}
log("HTTP OK " + result.code);
if (result.body) log("HTTP response: " + result.body);
} catch (e) {
log("ERROR HTTP callback: " + e);
}
});
} catch (e) {
log("ERROR HTTP.Request: " + e);
}
}
function hueGroupActionUrl(groupId) {
return HUE_URL + "api/" + HUE_USERNAME + "/groups/" + groupId + "/action";
}
function getSceneByButtonId(buttonId) {
for (let i = 0; i < SCENES.length; i++) {
let s = SCENES[i];
if (!s.enabled) continue;
if (s.button_id === buttonId) return s;
}
return null;
}
// ---- Recall scene ----
function recall_scene(scene) {
try {
if (!scene.sceneId || !scene.groupId || scene.groupId === "0") {
log("ERROR: Missing groupId/sceneId for: " + scene.name + " (button_id=" + scene.button_id + ")");
return;
}
log("Recalling SCENE (" + scene.name + ") sceneId=" + scene.sceneId + " on groupId=" + scene.groupId);
request({
method: "PUT",
url: hueGroupActionUrl(scene.groupId),
body: '{"scene":"' + scene.sceneId + '"}',
});
} catch (e) {
log("ERROR recall_scene (" + scene.name + "): " + e);
}
}
// ---- Virtual Button EVENT handler ----
Shelly.addEventHandler(function (e) {
try {
if (!e) return;
if (e.name !== "button") return;
if (!e.info || e.info.event !== "single_push") return;
let id = e.info.id; // Virtual Button ID (e.g. 200)
let scene = getSceneByButtonId(id);
if (!scene) return;
log("Button single_push -> " + scene.name + " (button_id=" + id + ")");
recall_scene(scene);
} catch (err) {
log("ERROR event handler: " + err);
}
});
// ---- Startup ----
log("Started. UI-only Hue SCENE controller via Virtual BUTTON events");
log("Total slots=" + SCENES.length);
let active = 0;
for (let i = 0; i < SCENES.length; i++) {
let s = SCENES[i];
if (s.enabled) {
active++;
log(
"ENABLED: button_id=" +
s.button_id +
" name=" +
s.name +
" groupId=" +
s.groupId +
" sceneId=" +
(s.sceneId ? "[set]" : "[missing]")
);
} else {
log("disabled: button_id=" + s.button_id + " name=" + s.name);
}
}
log("Enabled scenes=" + active);
*Script* - Add and control up to 4 Hue Groups
In the Shelly:
- Create two Button Virtuel Components for each group you want to control.
One button for ON, one button for OFF (Max 8 buttons = 4 groups total)
In the Script, type in/replace:
- Hue bridge IP adresss (HUE_URL)
- Username achieved from HUE Bridge (HUE_USERNAME)
- Enter the corresponding Group ID, VC ID, etc. (Under GROUPS =)
// Controls up to 4 Hue GROUPS using TWO Virtual BUTTONS per group (ON / OFF)
//
// WHY BUTTONS (not boolean):
// - Groups can be partially on/off due to manual changes
// - Buttons are idempotent: every press enforces the action
// - No polling, no state mismatch, no flip-trick
//
let HUE_URL = "http://192.168.2.52/"; // must end with /
let HUE_USERNAME = "MSzjJR7-HxXDMbKIdEJ7MR0JAqc5UuHmdk-oy6ts";
// ==================== GROUP CONFIG (max 4) ====================
//
// Each entry defines ONE Hue group with TWO Virtual Buttons.
//
// Fields:
// enabled : true/false -> whether this group is active
// name : string -> log-only name
// groupId : string -> Hue group/room/zone ID
// button_on_id : number -> Virtual Button ID for "ALL ON"
// button_off_id : number -> Virtual Button ID for "ALL OFF"
//
// ===============================================================
let GROUPS = [
{ enabled: true, name: "Living room", groupId: "3", button_on_id: 200, button_off_id: 201 },
{ enabled: false, name: "Kitchen", groupId: "1", button_on_id: 202, button_off_id: 203 },
{ enabled: false, name: "Bedroom", groupId: "2", button_on_id: 204, button_off_id: 205 },
{ enabled: false, name: "Office", groupId: "4", button_on_id: 206, button_off_id: 207 },
];
function log(msg) {
print("[HUE]", msg);
}
// ---- HTTP wrapper ----
function request(params) {
try {
log("HTTP " + params.method + " -> " + params.url);
if (params.body) log("HTTP body: " + params.body);
Shelly.call("HTTP.Request", params, function (res, errCode, errMsg) {
try {
if (errCode) {
log("HTTP ERROR " + errCode + ": " + errMsg);
return;
}
log("HTTP OK " + res.code);
if (res.body) log("HTTP response: " + res.body);
} catch (e) {
log("ERROR HTTP callback: " + e);
}
});
} catch (e) {
log("ERROR HTTP.Request: " + e);
}
}
function hueGroupActionUrl(groupId) {
return HUE_URL + "api/" + HUE_USERNAME + "/groups/" + groupId + "/action";
}
// ---- Find group by button id ----
function getGroupByButton(buttonId) {
try {
for (let i = 0; i < GROUPS.length; i++) {
let g = GROUPS[i];
if (!g.enabled) continue;
if (g.button_on_id === buttonId) return { group: g, action: true };
if (g.button_off_id === buttonId) return { group: g, action: false };
}
} catch (e) {
log("ERROR getGroupByButton: " + e);
}
return null;
}
// ---- Apply group ON/OFF ----
function setGroup(group, state) {
try {
if (!group || !group.groupId) {
log("ERROR setGroup: missing groupId");
return;
}
log("Group action -> " + group.name + " (groupId=" + group.groupId + ") = " + (state ? "ON" : "OFF"));
request({
method: "PUT",
url: hueGroupActionUrl(group.groupId),
body: state ? '{"on":true}' : '{"on":false}',
});
} catch (e) {
log("ERROR setGroup (" + (group ? group.name : "?") + "): " + e);
}
}
// ---- Virtual Button EVENT handler ----
Shelly.addEventHandler(function (e) {
try {
if (!e) return;
if (e.name !== "button") return;
if (!e.info || e.info.event !== "single_push") return;
let match = getGroupByButton(e.info.id);
if (!match) return;
log("Button single_push -> " + match.group.name + " [" + (match.action ? "ALL ON" : "ALL OFF") + "]");
setGroup(match.group, match.action);
} catch (err) {
log("ERROR event handler: " + err);
}
});
// ---- Startup ----
try {
log("Started. Hue GROUP controller (ON/OFF buttons)");
log("Total slots=" + GROUPS.length);
let active = 0;
for (let i = 0; i < GROUPS.length; i++) {
let g = GROUPS[i];
if (!g.enabled) continue;
active++;
log("ENABLED: name=" + g.name + " groupId=" + g.groupId + " onBtn=" + g.button_on_id + " offBtn=" + g.button_off_id);
}
log("Active groups=" + active);
} catch (e) {
log("ERROR during startup: " + e);
}
*Script* - Get status from Hue Motion Sensor
In the Shelly:
- Create one Boolean virtual component
In the Script, type in/replace:
- Hue bridge IP adresss (HUE_URL)
- Username achieved from HUE Bridge (HUE_USERNAME)
- Enter the corresponding Sensor ID, VC ID, etc. (Under SENSORS =)
- Optional you can also enable relay control
Note: The motion sensor status is polled every second (configurable). Due to the high load on the script engine, it is most likely not possible to run additional scripts on the same device.
// Poll up to 2* Hue motion sensors and write presence to Virtual Booleans
// * = POLL_INTERVAL_MS must be set at minimum 2 or else the script will crash
// Optionally control a local Shelly relay (switch) based on motion
// Hue endpoint: GET /api/<user>/sensors/<id> -> reads: state.presence
// Fully try/catch hardened
// Relay after-run supported (keep relay ON for X seconds after motion stops)
// after_run_s = 0 disables after-run (OFF immediately)
//*
let HUE_URL = "http://192.168.2.52/"; // must end with /
let HUE_USERNAME = "MSzjJR7-HxXDMbKIdEJ7MR0JAqc5UuHmdk-oy6ts";
// How often to poll (ms). 1000ms = 1 second (2 seconds must be minimum, if it listens for 2 sensors)
let POLL_INTERVAL_MS = 1000;
// Small stagger between sensor requests inside each poll cycle (ms)
let STAGGER_MS = 200;
// =============== MOTION SENSOR CONFIG (max 2) ===============
//
// Fields:
// use_sensor : true/false -> whether the script should poll this sensor
// name : string -> log-only label
// sensorId : string -> Hue sensor id from /api/<user>/sensors
// boolean_id : number -> Virtual Boolean id in Shelly to reflect presence
//
// Note:
// - We set boolean=true when motion is detected (presence=true)
// - We set boolean=false when no motion (presence=false)
//
// ============================================================
let SENSORS = [
{ use_sensor: true, name: "Hallway motion", sensorId: "27", boolean_id: 206 },
{ use_sensor: false, name: "Kitchen motion", sensorId: "18", boolean_id: 211 },
];
// ================= RELAY CONTROL (optional) =================
//
// use_relay : true/false -> enable/disable relay control
// switch_id : number -> Shelly switch id (usually 0)
// relay_on_motion : true/false -> true: relay ON when motion, OFF when no motion
// false: inverted (relay OFF when motion)
// relay_mode : "ANY" -> "ANY" means: ON if any enabled sensor has motion
// after_run_s : number -> keep relay ON this many seconds after motion stops
// 0 = disabled (OFF immediately)
//
// ============================================================
let RELAY = {
use_relay: false,
switch_id: 0,
relay_on_motion: true,
relay_mode: "ANY",
after_run_s: 0, // 0 = disabled
};
function log(msg) {
print("[HUE]", msg);
}
function logErr(msg) {
print("[HUE][ERROR]", msg);
}
// Cache last written boolean value per sensor
let _lastPresence = {}; // key: boolean_id -> true/false
// Cache last known motion state per sensor (for relay aggregation)
let _sensorMotion = {}; // key: sensorId -> true/false
// Cache last relay state to avoid spamming Switch.Set
let _lastRelayOn = null;
// After-run OFF timer id (null if none scheduled)
let _afterRunTimer = null;
function setBooleanIfChanged(booleanId, value) {
try {
value = !!value;
if (_lastPresence[booleanId] === value) return;
_lastPresence[booleanId] = value;
Shelly.call("Boolean.Set", { id: booleanId, value: value });
log("Boolean.Set id=" + booleanId + " -> " + value);
} catch (e) {
logErr("Boolean.Set (" + booleanId + "): " + e);
}
}
function computeAnyMotion() {
try {
// Relay rule: ANY enabled sensor with motion => true
let any = false;
for (let i = 0; i < SENSORS.length; i++) {
let s = SENSORS[i];
if (!s.use_sensor) continue;
let v = _sensorMotion[s.sensorId];
if (v === true) {
any = true;
break;
}
}
return any;
} catch (e) {
logErr("computeAnyMotion: " + e);
return false;
}
}
function setRelayIfEnabled(motionAny) {
try {
if (!RELAY.use_relay) return;
// Apply inversion rule
let wantOn = RELAY.relay_on_motion ? !!motionAny : !motionAny;
// ---------------- WANT ON ----------------
if (wantOn) {
// Cancel pending OFF timer
if (_afterRunTimer !== null) {
Timer.clear(_afterRunTimer);
_afterRunTimer = null;
log("After-run: OFF timer cancelled");
}
if (_lastRelayOn === true) return;
_lastRelayOn = true;
Shelly.call("Switch.Set", { id: RELAY.switch_id, on: true });
log("Switch.Set id=" + RELAY.switch_id + " -> ON");
return;
}
// ---------------- WANT OFF ----------------
let hold = RELAY.after_run_s || 0;
// After-run disabled → OFF immediately
if (hold <= 0) {
if (_lastRelayOn === false) return;
_lastRelayOn = false;
Shelly.call("Switch.Set", { id: RELAY.switch_id, on: false });
log("Switch.Set id=" + RELAY.switch_id + " -> OFF");
return;
}
// If already OFF, nothing to do
if (_lastRelayOn === false) return;
// Start OFF timer only once
if (_afterRunTimer !== null) return;
log("After-run: scheduling OFF in " + hold + "s");
_afterRunTimer = Timer.set(hold * 1000, false, function () {
try {
_afterRunTimer = null;
// Re-check motion before turning OFF
let stillMotion = computeAnyMotion();
if (stillMotion) {
log("After-run: motion returned, keeping relay ON");
return;
}
// Apply inversion rule again
let desiredOn = RELAY.relay_on_motion ? false : true;
if (_lastRelayOn === desiredOn) return;
_lastRelayOn = desiredOn;
Shelly.call("Switch.Set", { id: RELAY.switch_id, on: desiredOn });
log("Switch.Set id=" + RELAY.switch_id + " -> " + (desiredOn ? "ON" : "OFF"));
} catch (e) {
logErr("after-run timer: " + e);
}
});
} catch (e) {
logErr("setRelayIfEnabled: " + e);
}
}
function updateRelayFromSensors() {
try {
if (!RELAY.use_relay) return;
// Currently only "ANY" is implemented
let anyMotion = computeAnyMotion();
setRelayIfEnabled(anyMotion);
} catch (e) {
logErr("updateRelayFromSensors: " + e);
}
}
function request(params, cb, userdata) {
try {
Shelly.call(
"HTTP.Request",
params,
function (res, errCode, errMsg, ud) {
try {
if (errCode) {
logErr("HTTP ERROR " + errCode + ": " + errMsg);
return;
}
if (ud && ud.cb) ud.cb(res, ud.userdata);
} catch (e) {
logErr("HTTP callback: " + e);
}
},
{ cb: cb, userdata: userdata }
);
} catch (e) {
logErr("HTTP.Request: " + e);
}
}
function hueSensorUrl(sensorId) {
return HUE_URL + "api/" + HUE_USERNAME + "/sensors/" + sensorId;
}
function pollOne(sensor) {
try {
request(
{ method: "GET", url: hueSensorUrl(sensor.sensorId) },
function (res, sensor) {
try {
let data = JSON.parse(res.body);
let presence = !!(data.state && data.state.presence);
log("Motion (" + sensor.name + " id=" + sensor.sensorId + ") presence=" + presence);
// Update per-sensor boolean
setBooleanIfChanged(sensor.boolean_id, presence);
// Update per-sensor motion cache for relay
_sensorMotion[sensor.sensorId] = presence;
// Update relay based on all sensors (ANY rule)
updateRelayFromSensors();
} catch (e) {
logErr("parsing sensor (" + sensor.name + "): " + e);
}
},
sensor
);
} catch (e) {
logErr("pollOne (" + sensor.name + "): " + e);
}
}
function pollAll() {
try {
let idx = 0;
for (let i = 0; i < SENSORS.length; i++) {
let s = SENSORS[i];
if (!s.use_sensor) continue;
(function (sensor, n) {
Timer.set(n * STAGGER_MS, false, function () {
pollOne(sensor);
});
})(s, idx);
idx++;
}
} catch (e) {
logErr("pollAll: " + e);
}
}
// ---- Start polling loop ----
try {
log("Started. Hue motion poller. interval_ms=" + POLL_INTERVAL_MS);
for (let i = 0; i < SENSORS.length; i++) {
let s = SENSORS[i];
if (s.use_sensor) {
log("USING: " + s.name + " sensorId=" + s.sensorId + " -> boolean_id=" + s.boolean_id);
_sensorMotion[s.sensorId] = false; // initialize
} else {
log("SKIPPED: " + s.name);
}
}
if (RELAY.use_relay) {
log(
"Relay control enabled: switch_id=" +
RELAY.switch_id +
" relay_on_motion=" +
RELAY.relay_on_motion +
" mode=" +
RELAY.relay_mode +
" after_run_s=" +
(RELAY.after_run_s || 0)
);
} else {
log("Relay control disabled");
}
// immediate first poll
pollAll();
// repeating poll
Timer.set(POLL_INTERVAL_MS, true, function () {
pollAll();
});
} catch (e) {
logErr("startup: " + e);
}
Step 4 – Virtual Components
This example shows how to add virtual components (VCs) using the Shelly app.
The same steps can also be done from the web interface.
- From a device click the cube Icon.
- Click “Components”.
- Click “Create Virtual component”.

- Choose Boolean as type.
- Click Next.
- Set a name.
- Choose Toggle as view.
- Fill in the rest if you want.
- Repeat this for every light you want to add.
- Create a Group (Placed next to component)
- When creating a group, you choose which components are placed in it.
If the script is running and the correct information has been entered, you can control your Hue light from the VC icon on the Shelly device.
You can also use these controls in Shelly scenes and automations, but there are some limitations.
First, you must extract the virtual group as a device, which is supported.
However, Basic Shelly users can extract only one group. To extract up to 20 groups, a Shelly Premium subscription is required.

Step 5 – Virtual Device & Automations
Hue Extra Scripts
*Script* - Shelly input toggles Hue Light
Toggles a Hue light on or off from the physical input on the Shelly device where the script is running.
- Controls up to 4 Philips Hue lights
- input:0, input:1, input:2, input:3 each control one Hue light.
// Control up to 4 Hue lights (one per Shelly input)
// input:0 -> Hue light id
// input:1 -> Hue light id
// input:2 -> Hue light id
// input:3 -> Hue light id
let HUE_URL = "http://192.168.2.52/";
let HUE_USERNAME = "MSzjJR7-HxXDMbKIdEJ7MR0JAqc5UuHmdk-oy6ts";
// Map Shelly inputs to Hue light IDs
let INPUT_TO_HUE_LIGHT = {
"input:0": "3",
"input:1": "5",
"input:2": "9",
"input:3": "16",
};
function log(msg) { print("[HUE]", msg); }
function logErr(msg) { print("[HUE][ERROR]", msg); }
function request(params, callback, userdata) {
try {
log("HTTP " + params.method + " → " + params.url);
if (params.body) log("HTTP body: " + params.body);
Shelly.call("HTTP.Request", params, function (result, error_code, error_message, userdata) {
if (error_code) {
logErr("HTTP ERROR " + error_code + ": " + error_message);
return;
}
log("HTTP OK " + result.code);
if (userdata && userdata.callback) {
try {
userdata.callback(result, userdata.userdata);
} catch (e) {
logErr("Callback error: " + e);
}
}
}, { callback: callback, userdata: userdata });
} catch (e) {
logErr("Request setup failed: " + e);
}
}
function hueGetState(lightId, callback) {
try {
request(
{ method: "GET", url: HUE_URL + "api/" + HUE_USERNAME + "/lights/" + lightId },
function (result, cb) {
try {
let data = JSON.parse(result.body);
let isOn = data && data.state && data.state.on;
log("Hue light " + lightId + " is ON = " + isOn);
cb(isOn);
} catch (e) {
logErr("JSON parse / state error (light " + lightId + "): " + e);
}
},
callback
);
} catch (e) {
logErr("hueGetState failed: " + e);
}
}
function huePutState(lightId, state) {
try {
log("Setting Hue light " + lightId + " to: " + (state ? "ON" : "OFF"));
request({
method: "PUT",
url: HUE_URL + "api/" + HUE_USERNAME + "/lights/" + lightId + "/state",
body: state ? '{"on":true}' : '{"on":false}',
});
} catch (e) {
logErr("huePutState failed (light " + lightId + "): " + e);
}
}
function hueToggle(lightId) {
try {
log("Toggling Hue light " + lightId);
hueGetState(lightId, function (state) {
try {
huePutState(lightId, !state);
} catch (e) {
logErr("Toggle callback failed (light " + lightId + "): " + e);
}
});
} catch (e) {
logErr("hueToggle failed (light " + lightId + "): " + e);
}
}
Shelly.addEventHandler(function (event) {
try {
if (event.name !== "input") return;
let comp = event.component; // input:0 .. input:3
let lightId = INPUT_TO_HUE_LIGHT[comp];
if (!lightId) return;
log("Input " + comp + " raw: " + JSON.stringify(event.info));
let ev = event.info && event.info.event;
if (ev === "single_push") {
log("Action: single_push → toggle Hue light " + lightId);
hueToggle(lightId);
} else if (ev === "double_push") {
log("Action: double_push → force ON Hue light " + lightId);
huePutState(lightId, true);
} else if (ev === "long_push") {
log("Action: long_push → force OFF Hue light " + lightId);
huePutState(lightId, false);
} else {
log("Ignoring event (" + comp + "): " + ev);
}
} catch (e) {
logErr("Event handler crashed: " + e);
}
}); *Script* - Shelly input multi control Hue
Same as above, but with the option to control light, scene or group.
- Controls Philips Hue lights, groups, or scenes via a Shelly device
- Supports 4 inputs (input:0 – input:3)
- Each input can independently control a light, group, or scene
- Single / double / long press actions are configurable per input
- Uses local Hue Bridge API (no cloud required)
- Universal configuration fields for all inputs (unused fields are ignored)
// Philips Hue Bridge (local, no cloud)
// Supports controlling: light, group, or scene
// 4 inputs: input:0..input:3 each can have its own target + actions
// Universal target fields on ALL inputs: type, id, sceneId, groupId
// - light/group use target.id
// - scene uses target.sceneId + target.groupId
let HUE_URL = "http://192.168.2.52/"; // must end with /
let HUE_USERNAME = "MSzjJR7-HxXDMbKIdEJ7MR0JAqc5UuHmdk-oy6ts";
function log(msg) { print("[HUE]", msg); }
function logErr(msg) { print("[HUE][ERROR]", msg); }
// ---- Per-input configuration (same fields everywhere) ----
let INPUT_CONFIG = {
"input:0": {
target: { type: "light", id: "3", sceneId: "", groupId: "" },
actions: { single_push: "toggle", double_push: "on", long_push: "off" }
},
"input:1": {
target: { type: "light", id: "16", sceneId: "", groupId: "" },
actions: { single_push: "toggle", double_push: "on", long_push: "off" }
},
"input:2": {
target: { type: "scene", id: "", sceneId: "UbayCv-dHbdoKAHE", groupId: "13" },
actions: { single_push: "recall", double_push: "recall", long_push: "recall" }
},
"input:3": {
target: { type: "group", id: "13", sceneId: "", groupId: "" },
actions: { single_push: "on", double_push: "off", long_push: "toggle" }
}
};
// ---- HTTP wrapper ----
function request(params, callback, userdata) {
try {
log("HTTP " + params.method + " → " + params.url);
if (params.body) log("HTTP body: " + params.body);
Shelly.call("HTTP.Request", params, function (result, error_code, error_message, userdata) {
if (error_code) {
logErr("HTTP ERROR " + error_code + ": " + error_message);
return;
}
log("HTTP OK " + result.code);
if (result.body) log("HTTP response: " + result.body);
if (userdata && userdata.callback) {
try { userdata.callback(result, userdata.userdata); }
catch (e) { logErr("Callback error: " + e); }
}
}, { callback: callback, userdata: userdata });
} catch (e) {
logErr("Request setup failed: " + e);
}
}
// ---- Helpers: build Hue URLs ----
function hueLightUrl(lightId) {
return HUE_URL + "api/" + HUE_USERNAME + "/lights/" + lightId;
}
function hueGroupUrl(groupId) {
return HUE_URL + "api/" + HUE_USERNAME + "/groups/" + groupId;
}
// ---- Get current state (only meaningful for light/group) ----
function get_state(target, callback) {
try {
if (!target || !target.type) {
logErr("get_state: missing target/type");
return;
}
if (target.type === "scene") {
callback(false);
return;
}
if (!target.id) {
logErr("get_state: missing target.id for type=" + target.type);
return;
}
let url = (target.type === "light") ? hueLightUrl(target.id) : hueGroupUrl(target.id);
request({ method: "GET", url: url }, function (result, cb) {
try {
let data = JSON.parse(result.body);
if (target.type === "light") {
let isOn = data.state && data.state.on;
log("Hue light " + target.id + " state.on = " + isOn);
cb(!!isOn);
return;
}
// group
let anyOn = data.state && data.state.any_on;
log("Hue group " + target.id + " state.any_on = " + anyOn);
cb(!!anyOn);
} catch (e) {
logErr("JSON parse/state error: " + e);
}
}, callback);
} catch (e) {
logErr("get_state failed: " + e);
}
}
// ---- Set ON/OFF (light/group) ----
function put_onoff(target, state) {
try {
if (!target || !target.type) {
logErr("put_onoff: missing target/type");
return;
}
if (target.type === "scene") {
log("put_onoff ignored (type=scene). Use recall.");
return;
}
if (!target.id) {
logErr("put_onoff: missing target.id for type=" + target.type);
return;
}
if (target.type === "light") {
log("Setting Hue LIGHT " + target.id + " to: " + (state ? "ON" : "OFF"));
request({
method: "PUT",
url: hueLightUrl(target.id) + "/state",
body: state ? '{"on":true}' : '{"on":false}',
});
return;
}
// group
log("Setting Hue GROUP " + target.id + " to: " + (state ? "ON" : "OFF"));
request({
method: "PUT",
url: hueGroupUrl(target.id) + "/action",
body: state ? '{"on":true}' : '{"on":false}',
});
} catch (e) {
logErr("put_onoff failed: " + e);
}
}
// ---- Recall scene (scene) ----
// Hue API v1: activate scene via group action: PUT /groups/<groupId>/action {"scene":"<sceneId>"}
function recall_scene(target) {
try {
if (!target || target.type !== "scene") return;
if (!target.sceneId || !target.groupId) {
logErr("Scene target requires target.sceneId + target.groupId");
return;
}
log("Recalling Hue SCENE " + target.sceneId + " on GROUP " + target.groupId);
request({
method: "PUT",
url: hueGroupUrl(target.groupId) + "/action",
body: '{"scene":"' + target.sceneId + '"}',
});
} catch (e) {
logErr("recall_scene failed: " + e);
}
}
// ---- Execute mapped action ----
function doAction(target, action) {
try {
log("Action=" + action + " target=" + (target && target.type));
if (!target || !target.type) {
logErr("doAction: missing target/type");
return;
}
// Scene: only recall makes sense
if (target.type === "scene") {
recall_scene(target);
return;
}
if (action === "on") {
put_onoff(target, true);
} else if (action === "off") {
put_onoff(target, false);
} else if (action === "toggle") {
log("Toggle requested → reading current state first…");
get_state(target, function (state) {
try { put_onoff(target, !state); }
catch (e) { logErr("toggle callback failed: " + e); }
});
} else if (action === "recall") {
// If someone maps recall on non-scene, it won't do anything (safe)
recall_scene(target);
} else {
log("Unknown action: " + action);
}
} catch (e) {
logErr("doAction failed: " + e);
}
}
// ---- Optional: config sanity check (logs warnings, doesn't block) ----
function sanityCheck() {
try {
let keys = Object.keys(INPUT_CONFIG);
for (let i = 0; i < keys.length; i++) {
let k = keys[i];
let cfg = INPUT_CONFIG[k];
if (!cfg || !cfg.target || !cfg.target.type) {
logErr("Config " + k + ": missing target/type");
continue;
}
let t = cfg.target;
if ((t.type === "light" || t.type === "group") && !t.id) {
logErr("Config " + k + ": type=" + t.type + " requires target.id");
}
if (t.type === "scene" && (!t.sceneId || !t.groupId)) {
logErr("Config " + k + ": type=scene requires target.sceneId + target.groupId");
}
}
} catch (e) {
logErr("sanityCheck failed: " + e);
}
}
// ---- Input handler (handles input:0..3) ----
Shelly.addEventHandler(function (event) {
try {
if (event.name !== "input") return;
let comp = event.component; // input:0..input:3
let cfg = INPUT_CONFIG[comp];
if (!cfg) return;
log("Input " + comp + " raw: " + JSON.stringify(event.info));
let ev = event.info && event.info.event; // single_push, double_push, long_push...
let action = cfg.actions && cfg.actions[ev];
if (!action) {
log("No mapping for " + comp + " event=" + ev + " (ignored)");
return;
}
doAction(cfg.target, action);
} catch (e) {
logErr("Event handler crashed: " + e);
}
});
// Startup info
log("Started. Configured inputs: " + JSON.stringify(Object.keys(INPUT_CONFIG)));
sanityCheck();



