Is sending a POST or GET command to a device on the same network as the hub (via custom DH) not supported anymore?

EDIT: I PLUGGED MY LAN CABLE INTO THE WRONG NETWORK PORT ON MY SERVER TODAY! THIS MEANT IT HAD A NEW IP ADDRESS THAT WAS NOT DEFINED IN THE HANDLER. WHAT IS REALLY STUPID IS I CHANGED THE MAC ID TO MATCH, BUT FORGOT I HAD STATIC ROUTING SETUP ON MY ROUTER, SO A NEW IP WAS ASSIGNED. CODE WORKS, SO I’M LEAVING IT FOR OTHERS. NOT MY CODE, BUT I MODIFIED IT SOME AND IT WORKS FOR ME. THANKS GUYS!

I had a custom device handler I had working great under ST classic. Been out of the loop for a while.
that device handler used the code below, but it no longer works, and I’m stumped as to why. It’s supposed to forward to a node red flow i have on the same LAN as my ST v2 Hub. $ip is the local LAN address of the node red server, port is 1880. Debug log shows my device handler gets all the data to send, but it just doesn’t send it on the LAN anymore?

def headers = [:]
headers.put("HOST", "$ip:$port")
headers.put("Content-Type", "application/json")

def hubAction = new physicalgraph.device.HubAction(
    method: "POST",
    path: parsed.path,
    headers: headers,
    body: parsed.body
)

hubAction
log.debug "PREMISE DH: ${headers} ${parsed.path} ${parsed.body}"

If this code is inside a command method move the log statement before the hubAction statement. The hubAction has to be returned by the method.

2 Likes

Thanks. I just added the debug line today as I was finally wanting to get things working again.

The code was borrowed from a MQTT bridge. There is a custom device handler code (below), virtual devices (like switches), and an custom smartapp that acts as a bridge. In this manner, I was using SmartThings as a GUI for my custom home automation system.

I can still send updates and see the virtual devices change state, so commands from my home automation system are getting sent to the bridge app and processed.

The part that isn’t working is when I click on a virtual switch in the SmartThings app, I see the information in the debug log on the groovy portal, but it is not being sent over the local network like it used to. The code below had been working for years.

/**

  • Premise Bridge
  • Authors
  • Copyright 2016
  • Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except
  • in compliance with the License. You may obtain a copy of the License at:
  •  http://www.apache.org/licenses/LICENSE-2.0
    
  • Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
  • on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
  • for the specific language governing permissions and limitations under the License.
    */

import groovy.json.JsonSlurper
import groovy.json.JsonOutput

metadata {
definition (name: “PremiseBridgeDH”, namespace: “smartthings”, author: “ETC”) {
capability “Notification”
}

preferences {
    input("ip", "string",
        title: "Premise IP Address",
        description: "Premise IP Address",
        required: true,
        displayDuringSetup: true
    )
    input("port", "string",
        title: "Premise Port",
        description: "Premise Port",
        required: true,
        displayDuringSetup: true
    )
    input("mac", "string",
        title: "Premise MAC Address",
        description: "Premise MAC Address",
        required: true,
        displayDuringSetup: true
    )
}

simulator {}

tiles {
    valueTile("basic", "device.ip", width: 3, height: 2) {
        state("basic", label:'OK')
    }
    main "basic"
}

}

// Store the MAC address as the device ID so that it can talk to SmartThings
def setNetworkAddress() {
// Setting Network Device Id
def hex = “$settings.mac”.toUpperCase().replaceAll(‘:’, ‘’)
if (device.deviceNetworkId != “$hex”) {
device.deviceNetworkId = “$hex”
log.debug “Device Network Id set to ${device.deviceNetworkId}”
}
}

// Parse events from the Bridge
def parse(String description) {
setNetworkAddress()

// log.debug "PREMISE DH: parsing '${description}'"
def msg = parseLanMessage(description)

return createEvent(name: "message", value: new JsonOutput().toJson(msg.data))

}

// Send message to the Bridge
def deviceNotification(message) {
if (device.hub == null)
{
log.error “Hub is null, must set the hub in the device settings so we can get local hub IP and port”
return
}

// log.debug "PREMISE DH: Sending '${message}' to Premise."
setNetworkAddress()

def slurper = new JsonSlurper()
def parsed = slurper.parseText(message)

if (parsed.path == '/subscribe') {
    parsed.body.callback = device.hub.getDataValue("localIP") + ":" + device.hub.getDataValue("localSrvPortTCP")
}

def headers = [:]
headers.put("HOST", "$ip:$port")
headers.put("Content-Type", "application/json")

def hubAction = new physicalgraph.device.HubAction(
    method: "POST",
    path: parsed.path,
    headers: headers,
    body: parsed.body
)

hubAction

}

SmartApp that acts as a bridge:

/**

  • Premise Bridge
  • Authors
  • Copyright 2016
  • Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except
  • in compliance with the License. You may obtain a copy of the License at:
  •  http://www.apache.org/licenses/LICENSE-2.0
    
  • Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
  • on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
  • for the specific language governing permissions and limitations under the License.
    */
    import groovy.json.JsonSlurper
    import groovy.json.JsonOutput
    import groovy.transform.Field

// Massive lookup tree
@Field CAPABILITY_MAP = [
“accelerationSensors”: [
name: “Acceleration Sensor”,
capability: “capability.accelerationSensor”,
attributes: [
“acceleration”
]
],
“alarm”: [
name: “Alarm”,
capability: “capability.alarm”,
attributes: [
“alarm”
],
action: “actionAlarm”
],
“battery”: [
name: “Battery”,
capability: “capability.battery”,
attributes: [
“battery”
],
// ETC CHANGE ADDED ABILITY TO UPDATE BATTERY
action: “actionBattery”
],
“beacon”: [
name: “Beacon”,
capability: “capability.beacon”,
attributes: [
“presence”
]
],
“button”: [
name: “Button”,
capability: “capability.button”,
attributes: [
“button”
],
//ETC CHANGE ADDED BUTTON UPDATE CAPABILITY
action: “actionButton”
],
// ETC ADDED
“execute”: [
name: “Execute”,
capability: “capability.execute”,
attributes: [
“armMode”,
“data”
],
action: “actionExecute”
],
“carbonDioxideMeasurement”: [
name: “Carbon Dioxide Measurement”,
capability: “capability.carbonDioxideMeasurement”,
attributes: [
“carbonDioxide”
]
],
“carbonMonoxideDetector”: [
name: “Carbon Monoxide Detector”,
capability: “capability.carbonMonoxideDetector”,
attributes: [
“carbonMonoxide”
]
],
“colorControl”: [
name: “Color Control”,
capability: “capability.colorControl”,
attributes: [
“hue”,
“saturation”,
“color”
],
action: “actionColor”
],
“colorTemperature”: [
name: “Color Temperature”,
capability: “capability.colorTemperature”,
attributes: [
“colorTemperature”
],
action: “actionColorTemperature”
],
“consumable”: [
name: “Consumable”,
capability: “capability.consumable”,
attributes: [
“consumable”
],
action: “actionConsumable”
],
“contactSensors”: [
name: “Contact Sensor”,
capability: “capability.contactSensor”,
attributes: [
“contact”
],
// ETC CHANGE ADDED ABILITY TO UPDATE CONTACT SENSORS
action: “actionOpenClosed”
],
“doorControl”: [
name: “Door Control”,
capability: “capability.doorControl”,
attributes: [
“door”
],
action: “actionOpenClosed”
],
“energyMeter”: [
name: “Energy Meter”,
capability: “capability.energyMeter”,
attributes: [
“energy”
]
],
“garageDoors”: [
name: “Garage Door Control”,
capability: “capability.garageDoorControl”,
attributes: [
“door”
],
action: “actionOpenClosed”
],
// ETC CHANGE: ADD GEOLOCATION
“geolocations”: [
name: “Geolocation”,
capability: “capability.geolocation”,
attributes: [
“longitude”,
“latitude”,
“accuracy”,
“altitudeAccuracy”,
“speed”,
“lastUpdateTime”
],
],
“illuminanceMeasurement”: [
name: “Illuminance Measurement”,
capability: “capability.illuminanceMeasurement”,
attributes: [
“illuminance”
]
],
“imageCapture”: [
name: “Image Capture”,
capability: “capability.imageCapture”,
attributes: [
“image”
]
],
“levels”: [
name: “Switch Level”,
capability: “capability.switchLevel”,
attributes: [
“level”
],
action: “actionLevel”
],
// ETC CHANGE ADD AUDIO VOLUME
“volumes”: [
name: “Audio Volume”,
capability: “capability.audioVolume”,
attributes: [
“volume”
],
action: “actionVolume”
],
“lock”: [
name: “Lock”,
capability: “capability.lock”,
attributes: [
“lock”
],
action: “actionLock”
],
“mediaController”: [
name: “Media Controller”,
capability: “capability.mediaController”,
attributes: [
“activities”,
“currentActivity”
]
],
“motionSensors”: [
name: “Motion Sensor”,
capability: “capability.motionSensor”,
attributes: [
“motion”
],
action: “actionActiveInactive”
],
“musicPlayer”: [
name: “Music Player”,
capability: “capability.musicPlayer”,
attributes: [
“status”,
“level”,
“trackDescription”,
“trackData”,
“mute”
],
action: “actionMusicPlayer”
],
“pHMeasurement”: [
name: “pH Measurement”,
capability: “capability.pHMeasurement”,
attributes: [
“pH”
]
],
“powerMeters”: [
name: “Power Meter”,
capability: “capability.powerMeter”,
attributes: [
“power”
]
],
“presenceSensors”: [
name: “Presence Sensor”,
capability: “capability.presenceSensor”,
attributes: [
“presence”
],
action: “actionPresence”
],
“humiditySensors”: [
name: “Relative Humidity Measurement”,
capability: “capability.relativeHumidityMeasurement”,
attributes: [
“humidity”
]
],
“relaySwitch”: [
name: “Relay Switch”,
capability: “capability.relaySwitch”,
attributes: [
“switch”
],
action: “actionOnOff”
],
“shockSensor”: [
name: “Shock Sensor”,
capability: “capability.shockSensor”,
attributes: [
“shock”
]
],
“signalStrength”: [
name: “Signal Strength”,
capability: “capability.signalStrength”,
attributes: [
“lqi”,
“rssi”
]
],
“sleepSensor”: [
name: “Sleep Sensor”,
capability: “capability.sleepSensor”,
attributes: [
“sleeping”
]
],
“smokeDetector”: [
name: “Smoke Detector”,
capability: “capability.smokeDetector”,
attributes: [
“smoke”
]
],
“soundSensor”: [
name: “Sound Sensor”,
capability: “capability.soundSensor”,
attributes: [
“sound”
]
],
“stepSensor”: [
name: “Step Sensor”,
capability: “capability.stepSensor”,
attributes: [
“steps”,
“goal”
]
],
“switches”: [
name: “Switch”,
capability: “capability.switch”,
attributes: [
“switch”
],
action: “actionOnOff”
],
“soundPressureLevel”: [
name: “Sound Pressure Level”,
capability: “capability.soundPressureLevel”,
attributes: [
“soundPressureLevel”
]
],
“tamperAlert”: [
name: “Tamper Alert”,
capability: “capability.tamperAlert”,
attributes: [
“tamper”
]
],
“temperatureSensors”: [
name: “Temperature Measurement”,
capability: “capability.temperatureMeasurement”,
attributes: [
“temperature”
],
// ETC CHANGE ADDED ABILITY TO UPDATE TEMPERATURE
action: “actionTemperature”
],
“thermostat”: [
name: “Thermostat”,
capability: “capability.thermostat”,
attributes: [
“temperature”,
“heatingSetpoint”,
“coolingSetpoint”,
“thermostatSetpoint”,
“thermostatMode”,
“thermostatFanMode”,
“thermostatOperatingState”
],
action: “actionThermostat”
],
“thermostatCoolingSetpoint”: [
name: “Thermostat Cooling Setpoint”,
capability: “capability.thermostatCoolingSetpoint”,
attributes: [
“coolingSetpoint”
],
action: “actionCoolingThermostat”
],
“thermostatFanMode”: [
name: “Thermostat Fan Mode”,
capability: “capability.thermostatFanMode”,
attributes: [
“thermostatFanMode”
],
action: “actionThermostatFan”
],
“thermostatHeatingSetpoint”: [
name: “Thermostat Heating Setpoint”,
capability: “capability.thermostatHeatingSetpoint”,
attributes: [
“heatingSetpoint”
],
action: “actionHeatingThermostat”
],
“thermostatMode”: [
name: “Thermostat Mode”,
capability: “capability.thermostatMode”,
attributes: [
“thermostatMode”
],
action: “actionThermostatMode”
],
“thermostatOperatingState”: [
name: “Thermostat Operating State”,
capability: “capability.thermostatOperatingState”,
attributes: [
“thermostatOperatingState”
]
],
“thermostatSetpoint”: [
name: “Thermostat Setpoint”,
capability: “capability.thermostatSetpoint”,
attributes: [
“thermostatSetpoint”
]
],
“threeAxis”: [
name: “Three Axis”,
capability: “capability.threeAxis”,
attributes: [
“threeAxis”
]
],
“timedSession”: [
name: “Timed Session”,
capability: “capability.timedSession”,
attributes: [
“timeRemaining”,
“sessionStatus”
],
action: “actionTimedSession”
],
“touchSensor”: [
name: “Touch Sensor”,
capability: “capability.touchSensor”,
attributes: [
“touch”
]
],
“valve”: [
name: “Valve”,
capability: “capability.valve”,
attributes: [
“contact”
],
action: “actionOpenClosed”
],
“voltageMeasurement”: [
name: “Voltage Measurement”,
capability: “capability.voltageMeasurement”,
attributes: [
“voltage”
]
],
“waterSensors”: [
name: “Water Sensor”,
capability: “capability.waterSensor”,
attributes: [
“water”
]
],
“windowShades”: [
name: “Window Shade”,
capability: “capability.windowShade”,
attributes: [
“windowShade”
],
action: “actionOpenClosed”
]
]

definition(
name: “PremiseBridgeApp”,
namespace: “smartthings”,
author: “ETC”,
description: “A bridge between SmartThings and Premise”,
category: “My Apps”,
iconUrl: “https://s3.amazonaws.com/smartapp-icons/Connections/Cat-Connections.png”,
iconX2Url: “https://s3.amazonaws.com/smartapp-icons/Connections/Cat-Connections@2x.png”,
iconX3Url: “https://s3.amazonaws.com/smartapp-icons/Connections/Cat-Connections@3x.png
)

preferences {
section(“Send Notifications?”) {
input(“recipients”, “contact”, title: “Send notifications to”, multiple: true, required: false)
}

section ("Input") {
    CAPABILITY_MAP.each { key, capability ->
        input key, capability["capability"], title: capability["name"], multiple: true, required: false
    }
}

section ("Bridge") {
    input "bridge", "capability.notification", title: "Notify this Bridge", required: false, multiple: false
}

}

def installed() {
log.debug “Installed with settings: ${settings}”

//runEvery15Minutes(initialize)
initialize()

}

def updated() {
log.debug “Updated with settings: ${settings}”

// Unsubscribe from all events
unsubscribe()
// Subscribe to stuff
initialize()

}

// Return list of displayNames
def getDeviceNames(devices) {
def list =
devices.each{device->
list.push(device.displayName)
}
list
}

def initialize() {
// Subscribe to new events from devices
CAPABILITY_MAP.each { key, capability →
capability[“attributes”].each { attribute →
subscribe(settings[key], attribute, inputHandler)
}
}

// Subscribe to events from the bridge
subscribe(bridge, "message", bridgeHandler)

// Update the bridge
updateSubscription()

}

// Update the bridge"s subscription
def updateSubscription() {
def attributes = [
notify: [“Contacts”, “System”]
]
CAPABILITY_MAP.each { key, capability →
capability[“attributes”].each { attribute →
if (!attributes.containsKey(attribute)) {
attributes[attribute] =
}
settings[key].each {device →
attributes[attribute].push(device.displayName)
}
}
}

// ETC CHANGE: no need to do this for HTTP connection, this was for server to update MQTT

// def json = new groovy.json.JsonOutput().toJson([
// path: “/subscribe”,
// body: [
// devices: attributes
// ]
// ])

log.debug "PREMISE APP: Updating subscription: ${json}"

// bridge.deviceNotification(json)
}

// Receive an event from the bridge
// AKA Incoming command from Premise
def bridgeHandler(evt) {
def json = new JsonSlurper().parseText(evt.value)
log.debug “PREMISE APP: Received device event from bridge: ${json}”

if (json == null) {
} else {
if (json.type == “notify”) {
if (json.name == “Contacts”) {
sendNotificationToContacts(“${json.value}”, recipients)
} else {
sendNotificationEvent(“${json.value}”)
}
return
}

// @NOTE this is stored AWFUL, we need a faster lookup table
// @NOTE this also has no fast fail, I need to look into how to do that
CAPABILITY_MAP.each { key, capability ->
    if (capability["attributes"].contains(json.type)) {
        settings[key].each {device ->
            if (device.displayName == json.name) {
                if (json.command == false) {
                    if (device.getSupportedCommands().any {it.name == "setStatus"}) {
                        log.debug "PREMISE APP: Setting state ${json.type} = ${json.value}"
                        device.setStatus(json.type, json.value)
                        state.ignoreEvent = json;
                    }
                }
                else {
                    if (capability.containsKey("action")) {
                        def action = capability["action"]
                        log.debug "PREMISE APP: Setting action ${device} ${json.type} = ${json.value} method:${action}"
                        // ETC ADDED THE LINE BELOW TO HELP PREVENT CONTROL LOOP
                        state.ignoreEvent = json;
                        state.lastSent = json;
                        // Yes, this is calling the method dynamically
                        "$action"(device, json.type, json.value)
                    }
                }
            }
        }
    }
}
}

}

// Receive an event from a device
def inputHandler(evt) {
if (
state.ignoreEvent
&& state.ignoreEvent.name == evt.displayName
&& state.ignoreEvent.type == evt.name
&& state.ignoreEvent.value == evt.value
) {
log.debug “PREMISE APP: Ignoring event ${state.ignoreEvent}”
// ETC NOTES: reset ignore flag object
state.ignoreEvent = false;
} else if(
// ETC CHANGE: this was added for odd case where I arm alarm from Premise, but 3 events were still sent following ignore flag being cleared
// I think this is due to SmartHome Monitor setting these events again, and then being picked up.
state.lastSent
&& state.lastSent.name == evt.displayName
&& state.lastSent.type == evt.name
&& state.lastSent.value == evt.value
&& evt.name == “armMode”){

    log.debug "PREMISE APP: duplicate arm mode command ${state.lastSent}"
}
else {
	// ETC CHANGE attempt to send brightness instead of power state to avoid possible control loop
	def sDisplayName = evt.displayName
    def sValue = evt.value
    def sName = evt.name
    String sData = evt.data
    
    // ETC CHANGE: trick SmartThings Media Player into allowing undefined attributes (tried Execute capability like with alarm keypad, but no luck)
    if ((sValue == "playing") && (sName == "status")) {
    	try{
    		sValue = sData.substring(sData.indexOf("{((")+3,sData.indexOf("))}"))
        	log.debug "PREMISE APP: processing media zone undefined attribute: ${sValue}"
        } catch (StringIndexOutOfBoundsException e) {
        	log.debug "PREMISE APP: processing media zone attribute: ${sValue}"
        }
    }
    
    if ((sName == "latitude") || (sName == "longitude")) {
    	//def attrs = evt.device.supportedAttributes
        def attrs = evt.device.capabilities
  	attrs.each {
  		 //log.debug "TRACKER: attribute ${it.name}, values: ${it.values}"
         log.debug "TRACKER: capability ${it.name}"
			//log.debug "TRACKER: attribute ${it.name}, dataType: ${it.dataType}"
  		}
        
        //sLong = device.longitude
        //sLat = device.latitude
        //sAccuracy = device.accuracy
    	//log.debug "GEOLOCATION IS: Long:${sLong} LAT:${sLat} ACCURACY:${sAccuracy}"
    }
    
    // ETC NOTES: we finally send the json to Premise via the Premise Bridge device handler
    def json = new JsonOutput().toJson([
        path: "/push",
        body: [
            name: sDisplayName,
            value: sValue,
            type: sName
        ]
    ])

    log.debug "PREMISE APP: Forwarding device event to Premise: ${json}"
    bridge.deviceNotification(json)
}

}

// ±--------------------------------+
// | WARNING, BEYOND HERE BE DRAGONS |
// ±--------------------------------+
// These are the functions that handle incoming messages from Premise.
// I tried to put them in closures but apparently SmartThings Groovy sandbox
// restricts you from running clsures from an object (it’s not safe).

def actionAlarm(device, attribute, value) {
switch (value) {
case “strobe”:
device.strobe()
break
case “siren”:
device.siren()
break
case “off”:
device.off()
break
case “both”:
device.both()
break
}
}

def actionColor(device, attribute, value) {
switch (attribute) {
case “hue”:
device.setHue(value as float)
break
case “saturation”:
device.setSaturation(value as float)
break
case “color”:
def values = value.split(‘,’)
def colormap = [“hue”: values[0] as float, “saturation”: values[1] as float]
device.setColor(colormap)
break
}
}

def actionOpenClosed(device, attribute, value) {
if (value == “open”) {
device.open()
} else if (value == “closed”) {
device.close()
}
}

//ETC CHANGE ADDED BUTTON UPDATES FROM PREMISE
def actionButton(device, attribute, value) {
if (value == “pushed”) {
device.pushed()
} else if (value == “held”) {
device.held()
}
}

def actionOnOff(device, attribute, value) {
if (value == “off”) {
device.off()
} else if (value == “on”) {
device.on()
}
}

def actionActiveInactive(device, attribute, value) {
if (value == “active”) {
device.active()
} else if (value == “inactive”) {
device.inactive()
}
}

// ETC CHANGE ADD AUDIO VOLUME
def actionVolume(device, attribute, value) {
device.setVolume(value as int)
}

def actionThermostat(device, attribute, value) {
switch(attribute) {
case “heatingSetpoint”:
device.setHeatingSetpoint(value as int)
break
case “coolingSetpoint”:
device.setCoolingSetpoint(value as int)
break
case “thermostatMode”:
device.setThermostatMode(value)
break
case “thermostatFanMode”:
device.setThermostatFanMode(value)
break
// ETC CHANGE ADDED THESE SO PREMISE CAN UPDATE THEM
case “temperature”:
device.setTemperature(value as int)
break
case “thermostatSetpoint”:
device.setThermostatSetpoint(value as int)
break
case “thermostatOperatingState”:
device.setThermostatOperatingState(value)
}
}

def actionMusicPlayer(device, attribute, value) {
switch(attribute) {
case “level”:
device.setLevel(value)
break
case “mute”:
if (value == “muted”) {
device.mute()
} else if (value == “unmuted”) {
device.unmute()
}
break
case “status”:
if (device.getSupportedCommands().any {it.name == “setStatus”}) {
device.setStatus(value)
}
break
case “trackDescription”:
device.setTrackDescription(value)
}
}

def actionColorTemperature(device, attribute, value) {
device.setColorTemperature(value as int)
}

// ETC CHANGE: Added a call to setBatteryLevel as the SmartThings lock simulator supported this method.
def actionBattery(device, attribute, value) {
device.setBatteryLevel(value as int)
}

// ETC CHANGE: Added alarm keypad actions to execute
def actionExecute(device, attribute, value) {
switch(attribute) {
case “armMode”:
switch(value) {
case “disarmed”:
device.setDisarmed()
break
case “armedStay”:
device.setArmedStay()
break
case “armedAway”:
device.setArmedAway()
break
}
break
}
}

def actionLevel(device, attribute, value) {
device.setLevel(value as int)
}

def actionPresence(device, attribute, value) {
if (value == “present”) {
device.arrived();
}
else if (value == “not present”) {
device.departed();
}
}

def actionConsumable(device, attribute, value) {
device.setConsumableStatus(value)
}

def actionLock(device, attribute, value) {
if (value == “locked”) {
device.lock()
} else if (value == “unlocked”) {
device.unlock()
}
}

def actionCoolingThermostat(device, attribute, value) {
device.setCoolingSetpoint(value as int)
}

// ETC CHANGE: add setTemperature (supported by thermostat custom device handler)
def actionTemperature(device, attribute, value) {
device.setTemperature(value as int)
}

def actionThermostatFan(device, attribute, value) {
device.setThermostatFanMode(value)
}

def actionHeatingThermostat(device, attribute, value) {
device.setHeatingSetpoint(value as int)
}

def actionThermostatMode(device, attribute, value) {
device.setThermostatMode(value)
}

def actionTimedSession(device, attribute, value) {
if (attribute == “timeRemaining”) {
device.setTimeRemaining(value)
}
}

Just in case anyone ever wants to do something similar, this is the code of the virtual switch I used.

/**

  • Copyright 2015 SmartThings
  • Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except
  • in compliance with the License. You may obtain a copy of the License at:
  •  http://www.apache.org/licenses/LICENSE-2.0
    
  • Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
  • on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
  • for the specific language governing permissions and limitations under the License.

*/
metadata {
definition (name: “Premise Switch”, namespace: “smartthings”, author: “ETC”, ocfDeviceType: “oic.d.light”) {
capability “Actuator”
capability “Switch”
capability “Light”
command “on”
command “off”
}

tiles(scale: 2) {
multiAttributeTile(name:“switch”, type: “lighting”, width: 6, height: 4, canChangeIcon: true){
tileAttribute (“device.switch”, key: “PRIMARY_CONTROL”) {
attributeState “on”, label:‘${name}’, action:“switch.off”, icon:“st.Home.home30”, backgroundColor:“#00a0dc”, nextState:“off”
attributeState “off”, label:‘${name}’, action:“switch.on”, icon:“st.Home.home30”, backgroundColor:“#ffffff”, nextState:“on”
}
}
main([“switch”])
details([“switch”])
}
}

def parse(String description) {
log.debug “parse description: $description”
}

def on() {
sendEvent(name:“switch”, value: “on”)
log.debug “DEVICE: Premise virtual switch is on.”
}

def off() {
sendEvent(name:“switch”, value: “off”)
log.debug “DEVICE: Premise virtual switch is off.”
}