Automation not doing anything with custom device type

I have a raspberry Pi that I have wired up to control my curtains via the GPIO pins.
I can open and close them by running Python scripts on the Pi to set the GPIO pin values.

I integrated this with Smartthings using a custom device handler based on https://github.com/JZ-SmartThings/SmartThings/blob/master/Devices/Generic%20HTTP%20Device/GenericHTTPDevice.groovy.

My device is called curtains and the smartapp works fine - I click on the tile button and they open or close.

I wanted to set up an automatation to close the curtains at sunset for example. So I created an automation, set it to close curtains.

However, when I run the automation, it doesn’t do actually anything.

In the log I just see:
15:47:55: info HH execute(true, null, null), newMode: null

Whereas when I open or close them using the app I get logs from the device handler:
15:53:21: debug The method is POST
15:53:21: debug POST body is: CurtainsClose=
15:53:21: debug Uses which method: POST
15:53:21: debug The Header is [HOST:192.168.0.125:80, Content-Type:application/x-www-form-urlencoded]
15:53:21: debug The device id configured is: xxxxxxxxx

I’m sorry, I don’t understand your question. When you say “I wanted to set up an automation” what exactly do you mean? What smartapp are you trying to close the curtains with? Also, your curtains are recognized as curtains by ST? The device type you listed doesn’t have the capability of blinds or curtains. It does have the capability of switch. Have you tried using the switch capability whereever you’re building your “automation”?

Hi Ryan. Thanks for taking the time to reply. I’m using the Smartthings Classic app. The automation is the section where you set up routine to execute e.g. Good Morning, Good Night. You say what you want to happen and what time.
In my case I have my curtains set up with a custom device drive with both Window Shade and Garage Door capabilities. In the automation I select “open or close garage doors” as the action, then pick my device “curtains” and Action Close. I’ve also tried it as Window Shades, but nothing seems to happen.
This is my device handler. It invokes a php script on the raspberry Pi passing it command CurtainsOpen or CurtainsClose:

/**
 *  Curtains on pi
 *  Adapted from: https://github.com/JZ-SmartThings/SmartThings/blob/master/Devices/Generic%20HTTP%20Device/GenericHTTPDevice.groovy
  *
 *  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

metadata {
	definition (name: "Curtains", author: "Dominic McCormick", namespace:"regfixit") {
		capability "Window Shade"
        capability "Garage Door Control"
        attribute "lastmoved", "string"
		capability "Refresh"
        command "open"
        command "close"
   }

	preferences {
		input("DeviceIP", "string", title:"Device IP Address", description: "Please enter your device's IP Address", required: true, displayDuringSetup: true)
		input("DevicePort", "string", title:"Device Port", description: "Empty assumes port 80.", required: false, displayDuringSetup: true)
		input("DevicePath", "string", title:"URL Path", description: "Rest of the URL, include forward slash.", displayDuringSetup: true)
		input(name: "DevicePostGet", type: "enum", title: "POST or GET", options: ["POST","GET"], defaultValue: "POST", required: false, displayDuringSetup: true)
		input("UseOffVoiceCommandForCustom", "bool", title:"Use the OFF voice command (e.g. by Alexa) to control the Custom command? Assumed ON if MainTrigger is Momentary setting below is ON.", description: "", defaultValue: false, required: false, displayDuringSetup: true)
		input("UseJSON", "bool", title:"Use JSON instead of HTML?", description: "", defaultValue: false, required: false, displayDuringSetup: true)
		section() {
			input("HTTPAuth", "bool", title:"Requires User Auth?", description: "Choose if the HTTP requires basic authentication", defaultValue: false, required: true, displayDuringSetup: true)
			input("HTTPUser", "string", title:"HTTP User", description: "Enter your basic username", required: false, displayDuringSetup: true)
			input("HTTPPassword", "string", title:"HTTP Password", description: "Enter your basic password", required: false, displayDuringSetup: true)
		}
	}
	
	simulator {
	}

	tiles(scale: 2) {
		valueTile("lastmoved", "device.lastmoved", width: 4, height: 3, decoration: "flat") {
			state "default", label:'${currentValue}', backgroundColor:"#ffffff"
		}
		standardTile("Curtains", "device.GarageDoorControl", width: 2, height: 3, decoration: "flat") {
			state "open", label:'OPEN' , action: "close", icon: "st.Home.home9", backgroundColor:"#33cc33", nextState: "closing"
			state "closed", label: 'CLOSED', action: "open", icon: "st.Home.home9", backgroundColor: "#0066ff",  nextState: "opening"
			state "opening", label: 'OPENING', icon: "st.Home.home9", backgroundColor: "#ff9933", nextState: "opening"
			state "closing", label: 'CLOSING', icon: "st.Home.home9", backgroundColor: "#cc0000", nextState: "closing"
        }
		main "Curtains"
		details(["lastmoved", "Curtains"])
	}
}


def open() {
	log.debug "Executing 'open'"
	runCmd('CurtainsOpen=')
}
def close() {
	log.debug "Executing 'close'"
    runCmd('CurtainsClose=')
}

def refresh() {
	def FullCommand = 'Refresh='
	if (DeviceMainPin) {FullCommand=FullCommand+"&MainPin="+DeviceMainPin} //else {FullCommand=FullCommand+"&MainPin=4"}
	if (DeviceCustomPin) {FullCommand=FullCommand+"&CustomPin="+DeviceCustomPin} //else {FullCommand=FullCommand+"&CustomPin=21"}
	if (UseJSON==true) { FullCommand=FullCommand+"&UseJSON=" }
	runCmd(FullCommand)
}

def runCmd(String varCommand) {
	def host = DeviceIP
	def hosthex = convertIPtoHex(host).toUpperCase()
	def LocalDevicePort = ''
	if (DevicePort==null) { LocalDevicePort = "80" } else { LocalDevicePort = DevicePort }
	def porthex = convertPortToHex(LocalDevicePort).toUpperCase()
	device.deviceNetworkId = "$hosthex:$porthex"
	def userpassascii = "${HTTPUser}:${HTTPPassword}"
	def userpass = "Basic " + userpassascii.encodeAsBase64().toString()

	log.debug "The device id configured is: $device.deviceNetworkId"

	def headers = [:] 
	headers.put("HOST", "$host:$LocalDevicePort")
	headers.put("Content-Type", "application/x-www-form-urlencoded")
	if (HTTPAuth) {
		headers.put("Authorization", userpass)
	}
	log.debug "The Header is $headers"

	def path = ''
	def body = ''
	log.debug "Uses which method: $DevicePostGet"
	def method = "POST"
	try {
		if (DevicePostGet.toUpperCase() == "GET") {
			method = "GET"
			path = varCommand
			if (path.substring(0,1) != "/") { path = "/" + path }
			log.debug "GET path is: $path"
		} else {
			path = DevicePath
			body = varCommand 
			log.debug "POST body is: $body"
		}
		log.debug "The method is $method"
	}
	catch (Exception e) {
		settings.DevicePostGet = "POST"
		log.debug e
		log.debug "You must not have set the preference for the DevicePOSTGET option"
	}

	try {
		def hubAction = new physicalgraph.device.HubAction(
			method: method,
			path: path,
			body: body,
			headers: headers
			)
		hubAction.options = [outputMsgToS3:false]
		log.debug hubAction
		hubAction
	}
	catch (Exception e) {
		log.debug "Hit Exception $e on $hubAction"
	}
}

def parse(String description) {
//	log.debug "Parsing '${description}'"
	def whichTile = ''
	def map = [:]
	def retResult = []
	def descMap = parseDescriptionAsMap(description)
	def jsonlist = [:]
	def bodyReturned = ' '
	def headersReturned = ' '
	if (descMap["body"] && descMap["headers"]) {
		bodyReturned = new String(descMap["body"].decodeBase64())
		headersReturned = new String(descMap["headers"].decodeBase64())
	}
//	log.debug "BODY---" + bodyReturned
//	log.debug "HEADERS---" + headersReturned
	if (descMap["body"]) {
		if (headersReturned.contains("application/json")) {
			def body = new String(descMap["body"].decodeBase64())
			def slurper = new JsonSlurper()
			jsonlist = slurper.parseText(body)
			//log.debug "JSONLIST---" + jsonlist."CPU"
			jsonlist.put ("Date", new Date().format("yyyy-MM-dd h:mm:ss a", location.timeZone))
		} else if (headersReturned.contains("text/html")) {
			jsonlist.put ("Date", new Date().format("yyyy-MM-dd h:mm:ss a", location.timeZone))
			def data=bodyReturned.eachLine { line ->
				if (line.contains('CurtainsOpen=Success')) { jsonlist.put ("CurtainsOpen", "Success") }
				if (line.contains('CurtainsClose=Success')) { jsonlist.put ("CurtainsClose", "Success") }
			}
		}
	}
	if (descMap["body"] && (headersReturned.contains("application/json") || headersReturned.contains("text/html"))) {
		//putImageInS3(descMap)
		if (jsonlist."Refresh"=="Authentication Required!") {
			sendEvent(name: "refreshTriggered", value: "Use Authentication Credentials", unit: "")
			whichTile = 'refresh'
		}
		if (jsonlist."Refresh"=="Success") {
			sendEvent(name: "refreshTriggered", value: jsonlist."Date", unit: "")
			whichTile = 'refresh'
		}
		if (jsonlist."CurtainsOpen"=="Success") {
			sendEvent(name: "GarageDoorControl", value: "open", isStateChange: true)
            sendEvent(name: "lastmoved", value: "Opened at: " + jsonlist."Date", unit: "")
            whichTile = 'curtainsopen'
		}
		if (jsonlist."CurtainsClose"=="Success") {
			sendEvent(name: "GarageDoorControl", value: "closed", isStateChange: true)
            sendEvent(name: "lastmoved", value: "Closed at: " + jsonlist."Date", unit: "")
			whichTile = 'curtainsclose'
		}
        if (jsonlist."CustomTrigger"=="Authentication Required!") {
			sendEvent(name: "customTriggered", value: "Use Authentication Credentials", unit: "")
		}
		if (jsonlist."CustomTrigger"=="Success") {
			sendEvent(name: "customswitch", value: "on", isStateChange: true)
			sendEvent(name: "customTriggered", value: "MOMENTARY @ " + jsonlist."Date", unit: "")
			whichTile = 'customoff'
		}
		if (jsonlist."CustomTriggerOn"=="Success" && jsonlist."CustomPinStatus"==1) {
			sendEvent(name: "customTriggered", value: "ON @ " + jsonlist."Date", unit: "")
			whichTile = 'customon'
		}
		if (jsonlist."CustomTriggerOn"=="Authentication Required!") {
			sendEvent(name: "customTriggered", value: "Use Authentication Credentials", unit: "")
		}
		if (jsonlist."CustomTriggerOff"=="Success" && jsonlist."CustomPinStatus"==0) {
			sendEvent(name: "customTriggered", value: "OFF @ " + jsonlist."Date", unit: "")
			whichTile = 'customoff'
		}
		if (jsonlist."CustomTriggerOff"=="Authentication Required!") {
			sendEvent(name: "customTriggered", value: "Use Authentication Credentials", unit: "")
		}
		if (jsonlist."CustomPinStatus"==1) {
			sendEvent(name: "customswitch", value: "on", isStateChange: true)
			sendEvent(name: "refreshswitch", value: "default", isStateChange: true)
			whichTile = 'customon'
		}
		else if (jsonlist."CustomPinStatus"==0) {
			sendEvent(name: "customswitch", value: "off", isStateChange: true)
			sendEvent(name: "refreshswitch", value: "default", isStateChange: true)
			whichTile = 'customoff'
		}
		if (jsonlist."MainTrigger"=="Authentication Required!") {
			sendEvent(name: "mainTriggered", value: "Use Authentication Credentials", unit: "")
		}
		if (jsonlist."MainTrigger"=="Success") {
			sendEvent(name: "switch", value: "on", isStateChange: true)
			sendEvent(name: "mainTriggered", value: "MOMENTARY @ " + jsonlist."Date", unit: "")
			whichTile = 'mainoff'
		}
		if (jsonlist."MainTriggerOn"=="Success" && jsonlist."MainPinStatus"==1) {
			sendEvent(name: "mainTriggered", value: "ON @ " + jsonlist."Date", unit: "")
			whichTile = 'mainon'
		}
		if (jsonlist."MainTriggerOn"=="Authentication Required!") {
			sendEvent(name: "mainTriggered", value: "Use Authentication Credentials", unit: "")
		}
		if (jsonlist."MainTriggerOff"=="Success" && jsonlist."MainPinStatus"==0) {
			sendEvent(name: "mainTriggered", value: "OFF @ " + jsonlist."Date", unit: "")
			whichTile = 'mainoff'
		}
		if (jsonlist."MainTriggerOff"=="Authentication Required!") {
			sendEvent(name: "mainTriggered", value: "Use Authentication Credentials", unit: "")
			whichTile = 'mainoff'
		}
		if (jsonlist."MainPinStatus"==1) {
			sendEvent(name: "switch", value: "on", isStateChange: true)
			sendEvent(name: "refreshswitch", value: "default", isStateChange: true)
			whichTile = 'mainon'
		}
		else if (jsonlist."MainPinStatus"==0) {
			sendEvent(name: "switch", value: "off", isStateChange: true)
			sendEvent(name: "refreshswitch", value: "default", isStateChange: true)
			whichTile = 'mainoff'
		}
		if (jsonlist."CPU") {
			sendEvent(name: "cpuUsage", value: jsonlist."CPU".replace("=","\n").replace("%",""), unit: "")
		}
		if (jsonlist."Space Used") {
			sendEvent(name: "spaceUsed", value: jsonlist."Space Used".replace("=","\n").replace("%",""), unit: "")
		}
		if (jsonlist."UpTime") {
			sendEvent(name: "upTime", value: jsonlist."UpTime".replace("=","\n"), unit: "")
		}
		if (jsonlist."CPU Temp") {
			sendEvent(name: "cpuTemp", value: jsonlist."CPU Temp".replace("=","\n").replace("\'","°").replace("C ","C="), unit: "")
		}
		if (jsonlist."Free Mem") {
			sendEvent(name: "freeMem", value: jsonlist."Free Mem".replace("=","\n"), unit: "")
		}
		if (jsonlist."Temperature") {
			sendEvent(name: "temperature", value: jsonlist."Temperature".replace("=","\n").replace("\'","°").replace("C ","C="), unit: "")
			//String s = jsonlist."Temperature"
			//for(int i = 0; i < s.length(); i++)	{
			//   int c = s.charAt(i);
			//   log.trace "'${c}'\n"
			//}
		}
		if (jsonlist."Humidity") {
			sendEvent(name: "humidity", value: jsonlist."Humidity".replace("=","\n"), unit: "")
		}
		if (jsonlist."RebootNow") {
			whichTile = 'RebootNow'
		}
	}

	log.debug jsonlist

	//RESET THE DEVICE ID TO GENERIC/RANDOM NUMBER. THIS ALLOWS MULTIPLE DEVICES TO USE THE SAME ID/IP
	device.deviceNetworkId = "ID_WILL_BE_CHANGED_AT_RUNTIME_" + (Math.abs(new Random().nextInt()) % 99999 + 1)

	//RETURN BUTTONS TO CORRECT STATE
	log.debug 'whichTile: ' + whichTile
    switch (whichTile) {
        case 'refresh':
			sendEvent(name: "refreshswitch", value: "default", isStateChange: true)
			def result = createEvent(name: "refreshswitch", value: "default", isStateChange: true)
			//log.debug "refreshswitch returned ${result?.descriptionText}"
			return result
        case 'customoff':
			sendEvent(name: "customswitch", value: "off", isStateChange: true)
			def result = createEvent(name: "customswitch", value: "off", isStateChange: true)
			return result
        case 'customon':
			sendEvent(name: "customswitch", value: "on", isStateChange: true)
			def result = createEvent(name: "customswitch", value: "on", isStateChange: true)
			return result
        case 'mainoff':
			def result = createEvent(name: "switch", value: "off", isStateChange: true)
			return result
        case 'curtainsopen':
			def result = createEvent(name: "GarageDoorControl", value: "open", isStateChange: true)
			return result
        case 'curtainsclose':
			def result = createEvent(name: "GarageDoorControl", value: "closed", isStateChange: true)
			return result
        case 'mainon':
			def result = createEvent(name: "switch", value: "on", isStateChange: true)
			return result
        case 'RebootNow':
			sendEvent(name: "rebootnow", value: "default", isStateChange: true)
			def result = createEvent(name: "rebootnow", value: "default", isStateChange: true)
			return result
        default:
			sendEvent(name: "refreshswitch", value: "default", isStateChange: true)
			def result = createEvent(name: "refreshswitch", value: "default", isStateChange: true)
			//log.debug "refreshswitch returned ${result?.descriptionText}"
			return result
    }
}

def parseDescriptionAsMap(description) {
	description.split(",").inject([:]) { map, param ->
	def nameAndValue = param.split(":")
	map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
	}
}
private String convertIPtoHex(ipAddress) { 
	String hex = ipAddress.tokenize( '.' ).collect {  String.format( '%02x', it.toInteger() ) }.join()
	//log.debug "IP address entered is $ipAddress and the converted hex code is $hex"
	return hex
}
private String convertPortToHex(port) {
	String hexport = port.toString().format( '%04x', port.toInteger() )
	//log.debug hexport
	return hexport
}
private Integer convertHexToInt(hex) {
	Integer.parseInt(hex,16)
}
private String convertHexToIP(hex) {
	//log.debug("Convert hex to ip: $hex") 
	[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
}
private getHostAddress() {
	def parts = device.deviceNetworkId.split(":")
	//log.debug device.deviceNetworkId
	def ip = convertHexToIP(parts[0])
	def port = convertHexToInt(parts[1])
	return ip + ":" + port
}
  1. Are you sure you have the correct device selected?
  2. Are you sure this log is coming from that device?

The code you posted doesn’t seem to have any log that would produce this.

Hi Tony, I’m pretty sure I have the correct device selected in the Automation routine. The log isn’t coming frm the device. It appears when running the automation. Basically the automation routine says it has executed, but doesn’t seem to run anything in the device itself.