An updated version of the driver below from @TArman that includes dimmer capability, switch capability, set color for any or all modes, request power reporting, schedule power reporting, renamed buttons for their color, and increased to support seven colors.
import groovy.transform.Field
import groovy.json.JsonSlurper
metadata {
definition(
name: "Shelly RPC Color Picker",
namespace: "custom",
author: "You"
) {
capability "Switch"
capability "Actuator"
capability "SwitchLevel"
attribute "color", "string"
command "Power"
command "Reset"
command "Red"
command "Blue"
command "Magenta"
command "Yellow"
command "Green"
command "Orange"
command "White"
command "setLevel", [
[name:"Brightness*", description:"LED Brightness", type: "NUMBER"],
[name:"Which LED", type:"ENUM", description:"Where applied...", constraints: ["ON","OFF", "BOTH", "NIGHT"]] ]
command "polling", [
[name:"Rate*", description:"Poll rate in seconds",type: "INTEGER"]]
}
preferences {
input name: "deviceIp",
type: "text",
title: "Device IP Address",
defaultValue: "192.168.1.33",
required: true
input name: "requestTimeout",
type: "number",
title: "HTTP request timeout (seconds)",
defaultValue: 10,
range: "5..30"
input name: "logDesc",
type: "bool",
title: "Enable descriptive text logging",
defaultValue: true
input name: "logDebug",
type: "bool",
title: "Enable debug logging",
defaultValue: false
}
}
// Lifecycle methods
def installed() {
}
def updated() {
unschedule()
if (logDebug) runIn(1800, logsOff) // Auto-disable debug logs after 30 min
}
def logsOff() {
log.warn "Debug logging disabled"
device.updateSetting("logDebug", [value: "false", type: "bool"])
}
/* =========================
Button handlers
========================= */
def push(buttonNumber, String color) {
if (logDesc) log.info "Button ${buttonNumber} (${color}) pushed"
sendEvent(name: "color", value: color, isStateChange: true)
atomicState.LastColor = color
applyConfig(buttonNumber)
}
/* =========================
RPC SETTINGS
========================= */
private String getRpcUrl() {
String ip = settings.deviceIp ?: "192.168.20.102"
return "http://${ip}/rpc"
}
@Field static final String SET_METHOD = "PLUGS_UI.SetConfig"
@Field static final String GET_METHOD = "PLUGS_UI.GetConfig"
@Field static final String RPC_VERSION = "2.0"
/* =========================
CONFIG DEFINITIONS
========================= */
private static Map createLedConfig(List<Number> rgb) {
return [
leds: [
mode: "switch",
colors: [
"switch:0": [
on : [ rgb: rgb ],
off: [ rgb: rgb ]
]
]
]
]
}
private static Map createLedBrightBoth(Number level) {
return [
leds: [
mode: "switch",
colors: [
"switch:0": [
on : [ brightness: level ],
off: [ brightness: level ]
]
]
]
]
}
private static Map createLedBrightOn(Number level) {
return [
leds: [
mode: "switch",
colors: [
"switch:0": [
on : [ brightness: level ]
]
]
]
]
}
private static Map createLedBrightOff(Number level) {
return [
leds: [
mode: "switch",
colors: [
"switch:0": [
off: [ brightness: level ]
]
]
]
]
}
private static Map createLedBrightNight(Number level) {
return [
leds: [
night_mode: [
brightness: level
]
]
]
}
def Blue() { push(1, "Blue") }
def Magenta() { push(2, "Magenta") }
def Yellow() { push(3, "Yellow") }
def Green() { push(4, "Green") }
def Orange() { push(5, "Orange") }
def Red() { push(6, "Red") }
def White() { push(7, "White") }
private Map getConfigForButton(Integer btn) {
switch (btn) {
case 1:
return createLedConfig([0.0, 0.0, 100.0]) // Blue
case 2:
return createLedConfig([100.0, 0.0, 90.0]) // Magenta
case 3:
return createLedConfig([100.0, 100.0, 0.0]) // Yellow
case 4:
return createLedConfig([0.0, 100.0, 0.0]) // Green
case 5:
return createLedConfig([100.0, 50.0, 0.0]) // Orange
case 6:
return createLedConfig([100.0, 0.0, 0.0]) // Red
case 7:
return createLedConfig([100.0, 100.0, 100.0]) // White
default:
log.warn "No config defined for button ${btn}"
return null
}
}
/* =========================
RPC EXECUTION
========================= */
private void applyConfig(Integer buttonNumber) {
Map config = getConfigForButton(buttonNumber)
if (!config) return
Map rpcBody = [
jsonrpc: RPC_VERSION,
id : now(),
method : SET_METHOD,
params : [ config: config ]
]
sendRpc(rpcBody, "Applied config ${buttonNumber}")
}
private void setLevel(Number level) {
setLevel(level, "BOTH")
}
private void setLevel(Number level, String where) {
if (level < 0 || level > 100) {
log.warn "Brightness not 0...100"
return
}
Map config
if (where == "BOTH") {
config = createLedBrightBoth(level)
sendEvent(name: "OnLevel", value: level, isStateChange: true)
sendEvent(name: "OffLevel", value: level, isStateChange: true)
}
if (where == "ON") {
config = createLedBrightOn(level)
sendEvent(name: "OnLevel", value: level, isStateChange: true)
}
if (where == "OFF") {
config = createLedBrightOff(level)
sendEvent(name: "OffLevel", value: level, isStateChange: true)
}
if (where == "NIGHT") {
config = createLedBrightNight(level)
sendEvent(name: "NightLevel", value: level, isStateChange: true)
}
if (!config) return
Map rpcBody = [
jsonrpc: RPC_VERSION,
id : now(),
method : SET_METHOD,
params : [ config: config ]
]
String whereLevel = where + "level"
sendRpc(rpcBody, "Applied ${whereLevel}=${level}")
}
private void readCurrentConfig() {
Map rpcBody = [
jsonrpc: RPC_VERSION,
id : now(),
method : GET_METHOD,
params : [:]
]
sendRpc(rpcBody, "Read current config")
}
def on()
{
urlString = getRpcUrl() + "/Switch.Set?id=0&on=true"
String ans = urlString.toURL().text
sendSwitchEvents("on","digital",0)
if (logDesc) log.info "Turned on"
}
def off()
{
urlString = getRpcUrl() + "/Switch.Set?id=0&on=false"
String ans = urlString.toURL().text
sendSwitchEvents("off","digital",0)
if (logDesc) log.info "Turned off"
}
@Field static Integer pollRate = 22
def polling(Integer rate) {
pollRate = rate
atomicState.CurrentRate = rate
unschedule()
if (pollRate > 0) {
runIn(pollRate, Power)
if (logDesc) log.info "Power polling every ${pollRate} seconds"
} else {
if (logDesc) log.info "Power polling disabled"
}
}
def Power()
{
urlString = getRpcUrl() + "/Switch.GetStatus?id=0"
def jsonSlurper = new JsonSlurper()
def object = jsonSlurper.parseText(urlString.toURL().text)
/* aenergy:[total:137.099], apower:12.5, current:0.149, freq:60.0,*/
String energy = object.aenergy.total
String power = object.apower
if ((atomicState.MaxPower = null)|| (power > atomicState.MaxPower)) atomicState.MaxPower = power
String current = object.current
String volts = object.voltage
String freq = object.freq
sendEvent(name: "Voltage", value: volts, isStateChange: true)
sendEvent(name: "Energy", value: energy, isStateChange: true)
sendEvent(name: "Current", value: current, isStateChange: true)
sendEvent(name: "Power", value: power, isStateChange: true)
sendEvent(name: "Frequency", value: freq, isStateChange: true)
if (logDesc) log.info "${object}"
if (pollRate > 0) runIn(pollRate, Power)
}
def Reset()
{
urlString = getRpcUrl() + '/Switch.ResetCounters?id=0&type=["aenergy","ret_aenergy"]'
atomicState.MaxPower = 0
def jsonSlurper = new JsonSlurper()
def object = jsonSlurper.parseText(urlString.toURL().text)
if (logDesc) log.info "${object}"
}
/*******************************************************************
***** Event Senders
********************************************************************/
//evt = [name, value, type, unit, desc, isStateChange]
void sendEventLog(Map evt, Integer ep=0) {
//Set description if not passed in
evt.descriptionText = evt.desc ?: "${evt.name} set to ${evt.value} ${evt.unit ?: ''}".trim()
//Always send event to update last activity
evt.isStateChange = true
sendEvent(evt)
}
void sendSwitchEvents(value, String type, Integer ep=0) {
String desc = "switch is turned ${value}" + (type ? " (${type})" : "")
sendEventLog(name:"switch", value:value, type:type, desc:desc, ep)
}
void sendBasicButtonEvent(buttonId, String name) {
String desc = "button ${buttonId} ${name} (digital)"
sendEventLog(name:name, value:buttonId, type:"digital", desc:desc, isStateChange:true)
}
def String parse(String stuff) {}
def String checkWebsocketConnection (String stuff) {}
private void sendRpc(Map rpcBody, String descText) {
Map params = [
uri: getRpcUrl(),
requestContentType: "application/json",
contentType: "application/json",
body: rpcBody,
timeout: settings.requestTimeout ?: 10,
ignoreSSLIssues: true
]
try {
httpPost(params) { resp ->
if (resp.status == 200) {
if (logDesc) log.info descText
if (logDebug) {
log.debug "RPC request: ${rpcBody}"
log.debug "RPC response: ${resp.data}"
}
} else {
log.warn "RPC returned status ${resp.status}"
}
}
} catch (groovy.json.JsonException e) {
log.error "JSON parsing error: ${e.message}"
} catch (java.net.SocketTimeoutException e) {
log.error "Request timeout - check device connectivity"
} catch (Exception e) {
log.error "RPC failed: ${e.message}"
if (logDebug) log.debug "Full exception: ${e}"
}
}
Below is the original "Claude Optimized" updated driver Groovy code. Tarman's version above supersedes this.
import groovy.transform.Field
metadata {
definition(
name: "Shelly RPC HTTP POST Multi-Config (Claude Optimized)",
namespace: "custom",
author: "You"
) {
capability "Momentary"
capability "PushableButton"
command "applyConfig1"
command "applyConfig2"
command "applyConfig3"
command "applyConfig4"
command "applyConfig5"
}
preferences {
input name: "deviceIp",
type: "text",
title: "Device IP Address",
defaultValue: "192.168.20.102",
required: true
input name: "buttonCount",
type: "number",
title: "Number of enabled configs/buttons (1–5)",
defaultValue: 2,
range: "1..5",
required: true
input name: "enableReadBeforeWrite",
type: "bool",
title: "Read current config before writing",
defaultValue: false
input name: "requestTimeout",
type: "number",
title: "HTTP request timeout (seconds)",
defaultValue: 10,
range: "5..30"
input name: "logDesc",
type: "bool",
title: "Enable descriptive text logging",
defaultValue: true
input name: "logDebug",
type: "bool",
title: "Enable debug logging",
defaultValue: false
}
}
// Lifecycle methods
def installed() {
initialize()
}
def updated() {
initialize()
unschedule()
if (logDebug) runIn(1800, logsOff) // Auto-disable debug logs after 30 min
}
private void initialize() {
Integer count = Math.max(1, Math.min(5, settings.buttonCount ?: 2))
sendEvent(name: "numberOfButtons", value: count, isStateChange: true)
if (logDesc) log.info "Configured for ${count} buttons"
}
def logsOff() {
log.warn "Debug logging disabled"
device.updateSetting("logDebug", [value: "false", type: "bool"])
}
/* =========================
Button handlers
========================= */
def push(buttonNumber) {
Integer maxButtons = settings.buttonCount ?: 2
if (buttonNumber > maxButtons) {
if (logDebug) log.debug "Ignoring push for disabled button ${buttonNumber}"
return
}
if (logDesc) log.info "Button ${buttonNumber} pushed"
sendEvent(name: "pushed", value: buttonNumber, isStateChange: true)
applyConfig(buttonNumber)
}
def applyConfig1() { push(1) }
def applyConfig2() { push(2) }
def applyConfig3() { push(3) }
def applyConfig4() { push(4) }
def applyConfig5() { push(5) }
/* =========================
RPC SETTINGS
========================= */
private String getRpcUrl() {
String ip = settings.deviceIp ?: "192.168.20.102"
return "http://${ip}/rpc"
}
@Field static final String SET_METHOD = "PLUGS_UI.SetConfig"
@Field static final String GET_METHOD = "PLUGS_UI.GetConfig"
@Field static final String RPC_VERSION = "2.0"
/* =========================
CONFIG DEFINITIONS
========================= */
private static Map createLedConfig(List<Number> rgb) {
return [
leds: [
mode: "switch",
colors: [
"switch:0": [
on : [ rgb: rgb, brightness: 100 ],
off: [ rgb: rgb, brightness: 100 ]
]
]
]
]
}
private Map getConfigForButton(Integer btn) {
switch (btn) {
case 1:
return createLedConfig([0.0, 0.0, 100.0]) // Blue
case 2:
return createLedConfig([100.0, 0.0, 90.0]) // Magenta
case 3:
return createLedConfig([100.0, 100.0, 0.0]) // Yellow
case 4:
return createLedConfig([0.0, 100.0, 0.0]) // Green
case 5:
return createLedConfig([100.0, 50.0, 0.0]) // Orange
default:
log.warn "No config defined for button ${btn}"
return null
}
}
/* =========================
RPC EXECUTION
========================= */
private void applyConfig(Integer buttonNumber) {
Map config = getConfigForButton(buttonNumber)
if (!config) return
if (enableReadBeforeWrite) {
readCurrentConfig()
}
Map rpcBody = [
jsonrpc: RPC_VERSION,
id : now(),
method : SET_METHOD,
params : [ config: config ]
]
sendRpc(rpcBody, "Applied config ${buttonNumber}")
}
private void readCurrentConfig() {
Map rpcBody = [
jsonrpc: RPC_VERSION,
id : now(),
method : GET_METHOD,
params : [:]
]
sendRpc(rpcBody, "Read current config")
}
private void sendRpc(Map rpcBody, String descText) {
Map params = [
uri: getRpcUrl(),
requestContentType: "application/json",
contentType: "application/json",
body: rpcBody,
timeout: settings.requestTimeout ?: 10,
ignoreSSLIssues: true
]
try {
httpPost(params) { resp ->
if (resp.status == 200) {
if (logDesc) log.info descText
if (logDebug) {
log.debug "RPC request: ${rpcBody}"
log.debug "RPC response: ${resp.data}"
}
} else {
log.warn "RPC returned status ${resp.status}"
}
}
} catch (groovy.json.JsonException e) {
log.error "JSON parsing error: ${e.message}"
} catch (java.net.SocketTimeoutException e) {
log.error "Request timeout - check device connectivity"
} catch (Exception e) {
log.error "RPC failed: ${e.message}"
if (logDebug) log.debug "Full exception: ${e}"
}
}
A original question about renaming the buttons removed from here since Tarman's version of the driver took care of that.