Device Handler not remembering state


(cjcharles) #1

I have built a device handler to try and keep track of my alarm (Visonic Powermax), but am struggling to get the DH to remember the state. I can run configure/refresh/ping/… and they all call the same postAction which correctly gets the device status (and changes the various tile status’). However as soon as I exit that device page on the phone (or go to settings and back again), the device returns to the default status for all tiles.

Can anybody help me with what I am doing wrong/missing?

Thanks

import groovy.json.JsonSlurper

metadata {
	definition (name: "Visonic Controller", namespace: "cjcharles0", author: "Chris Charles") {
		capability "Switch"
		//capability "Refresh"
		capability "Sensor"
        capability "Configuration"
        //capability "Health Check"
        
        command "reset"
        
        command "ArmAway"
        command "ArmHome"
        command "Disarm"
     
	}

	simulator {
	}
    
    preferences {
        
        input("password", "password", title:"Password", required:false, displayDuringSetup:true)
        input("ip", "string", title:"IP Address", description: "e.g. 192.168.1.10", required: true, displayDuringSetup: true)
	}

	tiles (scale: 1){      

		standardTile("Status", "device.status", height: 1, width:1, inactiveLabel: false, canChangeIcon: false) {
            state "disarmed", label:"Disarmed", action:"", backgroundColor:"#D8D8D8"
            state "away", label:"Away", action:"", backgroundColor:"#FF9900"
            state "home", label:"Home", action:"", backgroundColor:"#FF9900"
            state "alarm", label:"Alarm", action:"", backgroundColor:"#FF0000"
        }
		standardTile("LastAction", "device.lastaction", height: 1, width:2, inactiveLabel: false, canChangeIcon: false) {
            state "zone", label:"Zone Action", action:"", backgroundColor:"#D8D8D8"
        }
        standardTile("ArmAway", "device.armaway", height: 1, width:1, inactiveLabel: false, canChangeIcon: true) {
            state "inactive", label:"Away", action:"ArmAway", backgroundColor:"#D8D8D8"
            state "changing", label:"Arming Away", action:"", backgroundColor:"#FF9900"
            state "active", label:"Armed Away", action:"", icon:"st.Outdoor.outdoor15", backgroundColor:"#00CC00"
        }
        standardTile("ArmHome", "device.armhome", height: 1, width: 1, inactiveLabel: false, canChangeIcon: true) {
            state "inactive", label:"Home", action:"ArmHome", backgroundColor:"#D8D8D8"
            state "changing", label:"Arming Home", action:"", backgroundColor:"#FF9900"
            state "active", label:"Armed Home", action:"", icon:"st.Home.home2", backgroundColor:"#00CC00"
        }
        standardTile("Disarm", "device.disarm", height: 1, width: 1, inactiveLabel: false, canChangeIcon: true) {
            state "inactive", label:"Disarm", action:"Disarm", icon:"st.presence.house.unlocked", backgroundColor:"#D8D8D8"
            state "changing", label:"Disarming", action:"", backgroundColor:"#FF9900"
            state "active", label:"Disarmed", action:"", icon:"st.locks.lock.unlocked", backgroundColor:"#00CC00"
        }

		standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 1, height: 1) {
			state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
		}
        
        standardTile("configure", "device.configure", inactiveLabel: false, width: 1, height: 1, decoration: "flat") {
			state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure"
		}
        

        valueTile("ip", "ip", decoration: "flat", width: 1, height: 1) {
    		state "ip", label:'IP Address\r\n${currentValue}'
		}
    }

	//main(["Status"])
	details(["Status", "LastAction" , "ArmAway", "ArmHome", "Disarm",
             "configure", "ip"])
}

def installed() {
	log.debug "installed()"
	configure()
}

def updated() {
	log.debug "updated()"
    configure()
}

def ArmAway() {
	log.debug "armaway()"
    sendEvent(name: "ArmAway", value: "changing", isStateChange: true)
    sendEvent(name: "ArmHome", value: "inactive", isStateChange: true)
    sendEvent(name: "Disarm", value: "inactive", isStateChange: true)
    postAction("/armaway")
}

def ArmHome() {
	log.debug "armhome()"
    sendEvent(name: "ArmAway", value: "inactive", isStateChange: true)
    sendEvent(name: "ArmHome", value: "changing", isStateChange: true)
    sendEvent(name: "Disarm", value: "inactive", isStateChange: true)
    postAction("/armhome")
}

def Disarm() {
	log.debug "disarm()"
    sendEvent(name: "ArmAway", value: "inactive", isStateChange: true)
    sendEvent(name: "ArmHome", value: "inactive", isStateChange: true)
    sendEvent(name: "Disarm", value: "changing", isStateChange: true)
    postAction("/disarm")
}

/*def configure() {
	log.debug "configure()"
	log.debug "Configuring Device For SmartThings Use"
    sendEvent(name: "checkInterval", value: 12 * 60, data: [protocol: "lan", hubHardwareId: device.hub.hardwareID], displayed: false)
    def responses = []
    if (ip != null) state.dni = setDeviceNetworkId(ip, "80")
    state.hubIP = device.hub.getDataValue("localIP")
    state.hubPort = device.hub.getDataValue("localSrvPortTCP")
    responses << configureStatus()
    //responses << configureInstant(state.hubIP, state.hubPort, powerOnState)
    //responses << configureDefault()
    return response(responses)
}*/

def configureVisonic(){
	log.debug "Configuring Visonic (IP/port....)"
}

def configure(){
	log.debug "Refreshing Visonic Information"
    return postAction("/status")

}

def parse(description) {
    def map = [:]
    def events = []
    def cmds = []
    
    if(description == "updated") return
    def descMap = parseDescriptionAsMap(description)

    def body = new String(descMap["body"].decodeBase64())

    def slurper = new JsonSlurper()
    def result = slurper.parseText(body)
    
    log.debug result
    
    if (result.containsKey("stat_str")) {
    	if (result.stat_str=="Disarmed") {
        	events << createEvent(name: "Disarm", value: "active", isStateChange: true)
            events << createEvent(name: "Status", value: "disarmed", isStateChange: true)
            log.debug "Disarmed Status found"}
        else if (result.stat_str=="Armed Away") {
        	events << createEvent(name: "ArmAway", value: "active", isStateChange: true)
            events << createEvent(name: "Status", value: "away", isStateChange: true)
            log.debug "Armed Away Status found"}
        else if (result.stat_str=="Armed Home") {
        	events << createEvent(name: "ArmHome", value: "active", isStateChange: true)
            events << createEvent(name: "Status", value: "home", isStateChange: true)
            log.debug "Armed Home Status found"}
    }

    if (result.containsKey("successtest")) {
       if (result.successtest == "true") state.configSuccess = "true" else state.configSuccess = "false" 
    }
    //if (cmds != [] && events != null) return [events, response(cmds)] else if (cmds != []) return response(cmds) else return events
    return events
}

//def reset() {
	//log.debug "reset()"
//}

def refresh() {
	log.debug "refresh()"
    postAction("/status")
}

def ping() {
    log.debug "ping()"
    postAction("/status")
}

private hex(value, width=2) {
	def s = new BigInteger(Math.round(value).toString()).toString(16)
	while (s.size() < width) {
		s = "0" + s
	}
	s
}

def sync(ip, port) {
    def existingIp = getDataValue("ip")
    def existingPort = getDataValue("port")
    if (ip && ip != existingIp) {
        updateDataValue("ip", ip)
        sendEvent(name: 'ip', value: ip)
    }
    if (port && port != existingPort) {
        updateDataValue("port", port)
    }
}

private encodeCredentials(username, password){
	def userpassascii = "${username}:${password}"
    def userpass = "Basic " + userpassascii.encodeAsBase64().toString()
    return userpass
}

private postAction(uri){ 
  log.debug "uri ${uri}"
  updateDNI()
  
  def userpass
  
  if(password != null && password != "") 
    userpass = encodeCredentials("admin", password)
    
  def headers = getHeader(userpass)
  
  def hubAction = new physicalgraph.device.HubAction(
    method: "GET",
    path: uri,
    headers: headers
  )
  hubAction    
}

private setDeviceNetworkId(ip, port = null){
    def myDNI
    if (port == null) {
        myDNI = ip
    } else {
  	    def iphex = convertIPtoHex(ip)
  	    def porthex = convertPortToHex(port)
        
        myDNI = "$iphex:$porthex"
    }
    log.debug "Device Network Id set to ${myDNI}"
    return myDNI
}

private updateDNI() { 
    if (state.dni != null && state.dni != "" && device.deviceNetworkId != state.dni) {
       device.deviceNetworkId = state.dni
    }
}

private getHostAddress() {
    if(getDeviceDataByName("ip") && getDeviceDataByName("port")){
        return "${getDeviceDataByName("ip")}:${getDeviceDataByName("port")}"
    }else{
	    return "${ip}:80"
    }
}

private String convertIPtoHex(ipAddress) { 
    String hex = ipAddress.tokenize( '.' ).collect {  String.format( '%02x', it.toInteger() ) }.join()
    return hex
}

private String convertPortToHex(port) {
	String hexport = port.toString().format( '%04x', port.toInteger() )
    return hexport
}

def parseDescriptionAsMap(description) {
	description.split(",").inject([:]) { map, param ->
		def nameAndValue = param.split(":")
		map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
	}
}

private getHeader(userpass = null){
    def headers = [:]
    headers.put("Host", getHostAddress())
    headers.put("Content-Type", "application/x-www-form-urlencoded")
    if (userpass != null)
       headers.put("Authorization", userpass)
    return headers
}

def toAscii(s){
        StringBuilder sb = new StringBuilder();
        String ascString = null;
        long asciiInt;
                for (int i = 0; i < s.length(); i++){
                    sb.append((int)s.charAt(i));
                    sb.append("|");
                    char c = s.charAt(i);
                }
                ascString = sb.toString();
                asciiInt = Long.parseLong(ascString);
                return asciiInt;
}

(ActionTiles.com co-founder Terry @ActionTiles; GitHub: @cosmicpuppy) #2

Just for good form, I’d recommend defining your State variables as Attributes up with the Commands… It’s a start, but probably not the root problem.

Also observe the state values in My Devices / device detail. And lots of console.log statements…


(cjcharles) #3

Thanks @tgauchat

Tried adding the attribute to each tile but sadly no difference. Also renamed the commands so that they arent the same as the tile names, but also sadly no difference.

Looking in the device detail doesnt show the devices losing state, its just like it is ignoring the status… The example below shows that Disarm should be in the active state, but it shows inactive if I open the device again.

Date	Source	Type	Name	Value	User	Displayed Text
2017-03-20 12:21:30.430 AM GMT
moments ago	DEVICE		Status	disarmed		Visonic Alarm status is disarmed
2017-03-20 12:21:30.430 AM GMT
moments ago	DEVICE		Disarm	active		Visonic Alarm disarm is active
2017-03-20 12:21:27.869 AM GMT
moments ago	COMMAND			configure		configure command was sent to Visonic Alarm
2017-03-20 12:21:15.733 AM GMT
moments ago	COMMAND			DoDisarm		DoDisarm command was sent to

(ActionTiles.com co-founder Terry @ActionTiles; GitHub: @cosmicpuppy) #4

Sorry for not having the opportunity to really dig into your code; but there’s always a high-probability in SmartThings that you’re running into an anomaly that even 100% “correct” code won’t fix. The “Groovy Sandbox” goes through another abstraction layer and that adds a bunch of quirks that are extremely difficult to diagnose without a real debugger (ain’t no such thing in the SmartThings IDE)…

I strongly recommend making a new copy of the DTH and stripping it down to the absolutely minimum “hello world” case. The code above, for example, includes concatenating sending multiple Events together – which may be (or not?) allowed by the spec … but it’s a complexity you need to take out of the picture until the basics are 100% working…

So start with “hello world” (i.e., send 1 event from 1 Tile) and then build up 1 more event at a time, each time observing and testing iteratively and stop as soon as it no longer works; go back, and try the incremental code a different way, until it works, etc…

Slow… but that’s how SmartThings development works best.


(Kevin) #5

There are a few problems:

  • Custom attributes must be declared in order to use them for tiles.

  • Groovy is case sensitive and your tiles are using attributes like “device.armaway”, but you’re creating events like “ArmAway”,

  • I’m not sure if you can have a command and an attribute with the same name so if you’re still having problems after making the changes above, rename the commands to something else.


(cjcharles) #6

So I have finally sussed it! Had to disable virtually everything before I worked out what was happening!

It seems that Groovy didnt like this:

    standardTile("ArmAway", "device.armaway", ......

Since it then obviously got confused about what I was referring to. I changed it to:

    standardTile("VArmAway", "armaway", ......

And then it started remembering the state correctly! I still think that is quite strange behaviour, but Im very glad to have got it sorted. I should also note that I changed the actions to VisonicArmAway aswell, but that didnt have any effect.

Thanks again for your help @krlaframboise and @tgauchat :smile:


How to change the state of a tile?
(ActionTiles.com co-founder Terry @ActionTiles; GitHub: @cosmicpuppy) #7

It’s not exactly “Groovy’s” disapproval :grimacing:

But rather the SmartThings UI & Device Control layer that methods like “standardTile()” implement.

The Docs pages for the Tiles leave a bit of ambiguity for various parameters. It’s best to look at a lot of examples.


(cjcharles) #8

Thanks again. Yes i would go so far as to say the docs leave masses to be
desired! Some of the key functions are hardly documented at all! As
frustrating as it made things, at least i feel like i have earned my beer!
:slight_smile: