Newbie question on state


(pascal) #1

hi there. I’m new to the SmartThings community so bear with me :slightly_smiling:

I would like to use a smart button to control a closet light with an old school pull string on it. to do this I’m using one of the new iris smart buttons (all ears if there is a better smart button). I forked off mitchpond repo @mitchp and installed the iris smart button device. after using it a bit I noticed it doesn’t always work. looking at the code I suspect the issue lies in the call to get the time when the button was pressed so that it can be compared to the time it was released (used to determine a press versus hold).

looking at the documentation (keep in mind I’m new):
there is a press in action and a release action. this is two calls on the device. To determine if the button was pressed or held, the time is stored on the press in action and then used on the release action to see how long the button was held in. The original code does a createEvent on the press in action which looking at the documentation I believe sends the event to the hub - suspect this should be storing the time into state so it is available for the release action. to try this I added the code between the //–new and //–new end with some debug.

unfortunately I seem to have missed as the get does not return the same value as the set.

what am I missing?

debug:

179938ad-5332-45d1-b1ed-cd61902bf478 10:08:42 PM EST: debug Button 1 pushed
179938ad-5332-45d1-b1ed-cd61902bf478 10:08:42 PM EST: debug get state.lpress=1457665653649
179938ad-5332-45d1-b1ed-cd61902bf478 10:08:42 PM EST: debug Parsing 'catchall: 0104 0006 01 01 0140 00 ED22 01 00 0000 00 00 '
179938ad-5332-45d1-b1ed-cd61902bf478 10:08:42 PM EST: debug set state.lPress=1457665722514
179938ad-5332-45d1-b1ed-cd61902bf478 10:08:42 PM EST: debug Parsing 'catchall: 0104 0006 01 01 0140 00 ED22 01 00 0000 01 00 ’

code:

/**
 *  Iris Smart Button
 *
 *  Copyright 2015 Mitch Pond
 *
 *  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: "Iris Smart Button", namespace: "mitchpond", author: "Mitch Pond") {
		capability "Battery"
		capability "Button"
        capability "Configuration"
        capability "Refresh"
		capability "Sensor"
        capability "Temperature Measurement"

		command "test"
        
        attribute "lastPress", "string"

		fingerprint endpointId: "01", profileId: "0104", inClusters: "0000,0001,0003,0007,0020,0402,0B05", outClusters: "0003,0006,0019", model:"3460-L", manufacturer: "CentraLite"
	}
    
    simulator {
   		status "button 2 pressed": "catchall: 0104 0006 02 01 0140 00 6F37 01 00 0000 01 00"
        status "button 2 released": "catchall: 0104 0006 02 01 0140 00 6F37 01 00 0000 00 00"
    }
    
    preferences{
    	input ("holdTime", "number", title: "Minimum time in seconds for a press to count as \"held\"",
        		defaultValue: 3, range: "3..15", displayDuringSetup: false)
        input ("tempOffset", "number", title: "Enter an offset to adjust the reported temperature",
        		defaultValue: 0, displayDuringSetup: false)
    }

	tiles(scale: 2) {
    	standardTile("button", "device.button", decoration: "flat", width: 2, height: 2) {
        	state "default", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#ffffff"//, action: "test()"
        }
		valueTile("battery", "device.battery", decoration: "flat", width: 2, height: 2) {
			state "battery", label:'${currentValue}% battery', unit:""
		}
        valueTile("temperature", "device.temperature", width: 2, height: 2) {
			state "temperature", label:'${currentValue}°',
				backgroundColors:[
					[value: 31, color: "#153591"],
					[value: 44, color: "#1e9cbb"],
					[value: 59, color: "#90d2a7"],
					[value: 74, color: "#44b621"],
					[value: 84, color: "#f1d801"],
					[value: 95, color: "#d04e00"],
					[value: 96, color: "#bc2323"]
				]
		}
        standardTile("refresh", "device.refresh", decoration: "flat", width: 2, height: 2) {
			state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
		}

		main (["temperature"])
		details(["button","temperature","battery","refresh"])
	}
}

def parse(String description) {
	log.debug "Parsing '${description}'"
    def descMap = zigbee.parseDescriptionAsMap(description)
    //log.debug descMap
    
	def results = []
    if (description?.startsWith('catchall:'))
		results = parseCatchAllMessage(descMap)
	else if (description?.startsWith('read attr -'))
		results = parseReportAttributeMessage(descMap)
    else if (description?.startsWith('temperature: '))
		results = parseCustomMessage(description)  
	return results;
}

def configure(){
	zigbee.onOffConfig() +											//on/off binding
    zigbee.configureReporting(1,0x20,0x20,3600,86400,0x01) + 		//battery reporting
    zigbee.configureReporting(0x0402,0x00,0x29,30,3600,0x0064) + 	//temperature reporting
    refresh()
}

def refresh(){
	return zigbee.readAttribute(0x0001,0x20) + zigbee.readAttribute(0x0402,0x00)
}

private Map parseCustomMessage(String description) {
    def value = zigbee.parseHATemperatureValue(description, "temperature: ", getTemperatureScale())
    return createTempEvent(value)
}

def parseCatchAllMessage(descMap) {
	//log.debug (descMap)
    if (descMap?.clusterId == "0006" && descMap?.command == "01") 		//button pressed
    	return createPressEvent(1)
    else if (descMap?.clusterId == "0006" && descMap?.command == "00") 	//button released
    	return [createButtonEvent(1), createEvent([name: 'lastPress', value: null, displayed: false])]
    else if (descMap?.clusterId == "0402" && descMap?.command == "01") 	//temperature response
    	return parseTempAttributeMsg(descMap)
}

def parseReportAttributeMessage(descMap) {
	if (descMap?.cluster == "0001" && descMap?.attrId == "0020") createBatteryEvent(getBatteryLevel(descMap.value))
    else if (descMap?.cluster == "0402" && descMap?.attrId == "0000") createTempEvent(getTemperature(descMap.value))
}

private parseTempAttributeMsg(descMap) {
	String temp = descMap.data[-2..-1].reverse().join()
    createTempEvent(getTemperature(temp))
}

private createTempEvent(value) {
	return createEvent(getTemperatureResult(value))
}

private createBatteryEvent(percent) {
	log.debug "Battery level at " + percent
	return createEvent([name: "battery", value: percent])
}

//this method determines if a press should count as a push or a hold and returns the relevant event type
private createButtonEvent(button) {
	def currentTime = now()
    def startOfPress = device.latestState('lastPress').date.getTime()
    //--new
    def newSTartOfPress=state.lpress
    log.debug "get state.lpress=${state.lPress}"
    //--end new
    def timeDif = currentTime - startOfPress
    def holdTimeMillisec = (settings.holdTime?:3).toInteger() * 1000
    
    if (timeDif < 0) {
    	log.debug "Press arrived out of sequence! Dropping event."
    	return []	//likely a message sequence issue. Drop this press and wait for another. Probably won't happen...
    }
    else if (timeDif > 20000) {
    	log.debug "Hold time longer than 20 seconds. Likely an error. Dropping event."
    	return []	//stale lastPress state. Likely an error
    }
    else if (timeDif < holdTimeMillisec) 
    	return createButtonPushedEvent(button)
    else 
    	return createButtonHeldEvent(button)
}

private createPressEvent(button) {
    //--new
    state.lPress=now()
    log.debug "set state.lPress=${state.lPress}"
    //--end new
	return createEvent([name: 'lastPress', value: now(), data:[buttonNumber: button], displayed: false])
}

private createButtonPushedEvent(button) {
	log.debug "Button ${button} pushed"
	return createEvent([
    	name: "button",
        value: "pushed", 
        data:[buttonNumber: button], 
        descriptionText: "${device.displayName} button ${button} was pushed",
        isStateChange: true, 
        displayed: true])
}

private createButtonHeldEvent(button) {
	log.debug "Button ${button} held"
	return createEvent([
    	name: "button",
        value: "held", 
        data:[buttonNumber: button], 
        descriptionText: "${device.displayName} button ${button} was held",
        isStateChange: true])
}

private getBatteryLevel(rawValue) {
	def intValue = Integer.parseInt(rawValue,16)
	def min = 2.1
    def max = 3.0
    def vBatt = intValue / 10
    return ((vBatt - min) / (max - min) * 100) as int
}

def getTemperature(value) {
	def celsius = Integer.parseInt(value, 16).shortValue() / 100
	if(getTemperatureScale() == "C"){
		return celsius
	} else {
		return celsiusToFahrenheit(celsius) as Integer
	}
}

private Map getTemperatureResult(value) {
	log.debug 'TEMP'
	def linkText = getLinkText(device)
	if (tempOffset) {
		def offset = tempOffset as int
		def v = value as int
		value = v + offset
	}
	def descriptionText = "${linkText} was ${value}°${temperatureScale}"
    log.debug "Temp value: "+value
	return [
		name: 'temperature',
		value: value,
		descriptionText: descriptionText
	]
}

// handle commands
def test() {
    log.debug "Test"
	//zigbee.refreshData("0","4") + zigbee.refreshData("0","5") + zigbee.refreshData("1","0x0020")
    zigbee.refreshData("0x402","0")
}

(Mitch Pond) #2

It should work as is, but I am rewriting these to use a state variable instead of an attribute. Probably this weekend.


(pascal) #3

awesome! I’m just trying to learn how the smartThings platform works and this seemed like a good starting point :slightly_smiling:

In reading the API I understand the createEvent better now - it is setting a custom attribute which you then later use - essentially the same thing as using state except that it would be viewable outside the device handler compared to state which is scoped only to the device handler - did I get that right?

to learn I was playing around [sorry @mitchp your code happened to be where I decided to start learning :)] - it appears that the first call to get the attribute consistently does not return the value set by the createEvent - my subsequent calls however do

also I noticed that my experimentation with using state is consistently returning the state that was set on the previous press/release cycle - in other words it is the before-last value, not the one most recently set.

The log below shows another interesting event - the get commands returned NULL???

179938ad-5332-45d1-b1ed-cd61902bf478  9:47:49 AM EST: debug Hold time longer than 20 seconds. Likely an error. Dropping event.
179938ad-5332-45d1-b1ed-cd61902bf478  9:47:49 AM EST: debug startOfPress=1457707445456 //where is this coming from?
179938ad-5332-45d1-b1ed-cd61902bf478  9:47:49 AM EST: debug state.lpress=1457707445636 //BAD - set to value from previous on/off
179938ad-5332-45d1-b1ed-cd61902bf478  9:47:49 AM EST: debug device.latestState('lastPress').date.getTime()=null
179938ad-5332-45d1-b1ed-cd61902bf478  9:47:49 AM EST: debug device.latestState('lastPress').value=null
179938ad-5332-45d1-b1ed-cd61902bf478  9:47:49 AM EST: debug device.currentState('lastPress').date.getTime()=null
179938ad-5332-45d1-b1ed-cd61902bf478  9:47:49 AM EST: debug device.currentState('lastPress').value=null
179938ad-5332-45d1-b1ed-cd61902bf478  9:47:49 AM EST: debug createButtonEvent(button):entry
179938ad-5332-45d1-b1ed-cd61902bf478  9:47:49 AM EST: debug parseCatchAllMessage(descMap):entry
179938ad-5332-45d1-b1ed-cd61902bf478  9:47:49 AM EST: debug Parsing 'catchall: 0104 0006 01 01 0140 00 ED22 01 00 0000 00 00 '
179938ad-5332-45d1-b1ed-cd61902bf478  9:47:49 AM EST: debug setting state and attribute time to =1457707669609  //NOTE: this is the time that should show for current state and state call on release
179938ad-5332-45d1-b1ed-cd61902bf478  9:47:49 AM EST: debug createPressEvent(button):entry
179938ad-5332-45d1-b1ed-cd61902bf478  9:47:49 AM EST: debug parseCatchAllMessage(descMap):entry
179938ad-5332-45d1-b1ed-cd61902bf478  9:47:49 AM EST: debug Parsing 'catchall: 0104 0006 01 01 0140 00 ED22 01 00 0000 01 00 '
--------------------------------------------------------------------------------------------------
179938ad-5332-45d1-b1ed-cd61902bf478  9:44:05 AM EST: debug Button 1 pushed
179938ad-5332-45d1-b1ed-cd61902bf478  9:44:05 AM EST: debug startOfPress=1457707445307 //BAD - this is the first time calling device.latestState('lastPress').date.getTime() - returned a different value
179938ad-5332-45d1-b1ed-cd61902bf478  9:44:05 AM EST: debug state.lpress=1457707411351 //BAD - set to value from previous on/off
179938ad-5332-45d1-b1ed-cd61902bf478  9:44:05 AM EST: debug device.latestState('lastPress').date.getTime()=1457707445636 //GOOD
179938ad-5332-45d1-b1ed-cd61902bf478  9:44:05 AM EST: debug device.latestState('lastPress').value=1457707445636 //GOOD
179938ad-5332-45d1-b1ed-cd61902bf478  9:44:05 AM EST: debug device.currentState('lastPress').date.getTime()=1457707445636 //GOOD
179938ad-5332-45d1-b1ed-cd61902bf478  9:44:05 AM EST: debug device.currentState('lastPress').value=1457707445636 //GOOD
179938ad-5332-45d1-b1ed-cd61902bf478  9:44:05 AM EST: debug createButtonEvent(button):entry
179938ad-5332-45d1-b1ed-cd61902bf478  9:44:05 AM EST: debug parseCatchAllMessage(descMap):entry
179938ad-5332-45d1-b1ed-cd61902bf478  9:44:05 AM EST: debug Parsing 'catchall: 0104 0006 01 01 0140 00 ED22 01 00 0000 00 00 '
179938ad-5332-45d1-b1ed-cd61902bf478  9:44:05 AM EST: debug setting state and attribute time to =1457707445636  //NOTE: this is the time that should show for current state and state call on release
179938ad-5332-45d1-b1ed-cd61902bf478  9:44:05 AM EST: debug createPressEvent(button):entry
179938ad-5332-45d1-b1ed-cd61902bf478  9:44:05 AM EST: debug parseCatchAllMessage(descMap):entry
179938ad-5332-45d1-b1ed-cd61902bf478  9:44:05 AM EST: debug Parsing 'catchall: 0104 0006 01 01 0140 00 ED22 01 00 0000 01 00 '

(pascal) #4

[CODE]
/**

  • Iris Smart Button
  • Copyright 2015 Mitch Pond
  • 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: “Iris Smart Button”, namespace: “mitchpond”, author: “Mitch Pond”) {
capability "Battery"
capability "Button"
capability "Configuration"
capability "Refresh"
capability "Sensor"
capability “Temperature Measurement”

	command "test"
    
    attribute "lastPress", "string"

	fingerprint endpointId: "01", profileId: "0104", inClusters: "0000,0001,0003,0007,0020,0402,0B05", outClusters: "0003,0006,0019", model:"3460-L", manufacturer: "CentraLite"
}

simulator {
	status "button 2 pressed": "catchall: 0104 0006 02 01 0140 00 6F37 01 00 0000 01 00"
    status "button 2 released": "catchall: 0104 0006 02 01 0140 00 6F37 01 00 0000 00 00"
}

preferences{
	input ("holdTime", "number", title: "Minimum time in seconds for a press to count as \"held\"",
    		defaultValue: 3, range: "3..15", displayDuringSetup: false)
    input ("tempOffset", "number", title: "Enter an offset to adjust the reported temperature",
    		defaultValue: 0, displayDuringSetup: false)
}

tiles(scale: 2) {
	standardTile("button", "device.button", decoration: "flat", width: 2, height: 2) {
    	state "default", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#ffffff"//, action: "test()"
    }
	valueTile("battery", "device.battery", decoration: "flat", width: 2, height: 2) {
		state "battery", label:'${currentValue}% battery', unit:""
	}
    valueTile("temperature", "device.temperature", width: 2, height: 2) {
		state "temperature", label:'${currentValue}°',
			backgroundColors:[
				[value: 31, color: "#153591"],
				[value: 44, color: "#1e9cbb"],
				[value: 59, color: "#90d2a7"],
				[value: 74, color: "#44b621"],
				[value: 84, color: "#f1d801"],
				[value: 95, color: "#d04e00"],
				[value: 96, color: "#bc2323"]
			]
	}
    standardTile("refresh", "device.refresh", decoration: "flat", width: 2, height: 2) {
		state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
	}

	main (["temperature"])
	details(["button","temperature","battery","refresh"])
}

}

def parse(String description) {
log.debug "Parsing ‘${description}’"
def descMap = zigbee.parseDescriptionAsMap(description)
//log.debug descMap
//descMap.each {ent ->
// log.debug “description map: ${ent.key} ${ent.value}”
//}

def results = []
if (description?.startsWith('catchall:'))
	results = parseCatchAllMessage(descMap)
else if (description?.startsWith('read attr -'))
	results = parseReportAttributeMessage(descMap)
else if (description?.startsWith('temperature: '))
	results = parseCustomMessage(description)  
return results;

}

def configure(){
zigbee.onOffConfig() + //on/off binding
zigbee.configureReporting(1,0x20,0x20,3600,86400,0x01) + //battery reporting
zigbee.configureReporting(0x0402,0x00,0x29,30,3600,0x0064) + //temperature reporting
refresh()
}

def refresh(){
return zigbee.readAttribute(0x0001,0x20) + zigbee.readAttribute(0x0402,0x00)
}

private Map parseCustomMessage(String description) {
def value = zigbee.parseHATemperatureValue(description, "temperature: ", getTemperatureScale())
return createTempEvent(value)
}

def parseCatchAllMessage(descMap) {
log.debug “parseCatchAllMessage(descMap):entry”
//log.debug (descMap)
if (descMap?.clusterId == “0006” && descMap?.command == “01”) //button pressed
return createPressEvent(1)
else if (descMap?.clusterId == “0006” && descMap?.command == “00”) //button released
return [createButtonEvent(1), createEvent([name: ‘lastPress’, value: null, displayed: false])]
else if (descMap?.clusterId == “0402” && descMap?.command == “01”) //temperature response
return parseTempAttributeMsg(descMap)
}

def parseReportAttributeMessage(descMap) {
if (descMap?.cluster == “0001” && descMap?.attrId == “0020”) createBatteryEvent(getBatteryLevel(descMap.value))
else if (descMap?.cluster == “0402” && descMap?.attrId == “0000”) createTempEvent(getTemperature(descMap.value))
}

private parseTempAttributeMsg(descMap) {
String temp = descMap.data[-2…-1].reverse().join()
createTempEvent(getTemperature(temp))
}

private createTempEvent(value) {
return createEvent(getTemperatureResult(value))
}

private createBatteryEvent(percent) {
log.debug "Battery level at " + percent
return createEvent([name: “battery”, value: percent])
}

//this method determines if a press should count as a push or a hold and returns the relevant event type
private createButtonEvent(button) {
log.debug "createButtonEvent(button):entry"
def currentTime = now()
def startOfPress = device.latestState(‘lastPress’).date.getTime()
//–new---------------------------------

//attribute debug - get attribue current and last using value and getTime
def curLastPress = device.currentState('lastPress').value
def curLastPressDate = device.currentState('lastPress').date.getTime()
def latestLastPress = device.latestState('lastPress').value
def latestLastPressDate = device.latestState('lastPress').date.getTime()
log.debug "device.currentState('lastPress').value=${latestLastPress}"
log.debug "device.currentState('lastPress').date.getTime()=${latestLastPress}"
log.debug "device.latestState('lastPress').value=${latestLastPress}"
log.debug "device.latestState('lastPress').date.getTime()=${latestLastPress}"

//state based debug
def newSTartOfPress=state.lpress
log.debug "state.lpress=${state.lPress}"

//--end new-----------------------------


//--new---------------------------------
//debug the startOfPress which was set on the first get latest state call
log.debug "startOfPress=${startOfPress}"
//--end new-----------------------------
    
    
def timeDif = currentTime - startOfPress
def holdTimeMillisec = (settings.holdTime?:3).toInteger() * 1000

if (timeDif < 0) {
	log.debug "Press arrived out of sequence! Dropping event."
	return []	//likely a message sequence issue. Drop this press and wait for another. Probably won't happen...
}
else if (timeDif > 20000) {
	log.debug "Hold time longer than 20 seconds. Likely an error. Dropping event."
	return []	//stale lastPress state. Likely an error
}
else if (timeDif < holdTimeMillisec) 
	return createButtonPushedEvent(button)
else 
	return createButtonHeldEvent(button)

}

private createPressEvent(button) {
log.debug “createPressEvent(button):entry”
//–new
def curTime = now() //get the current time to use as debug
log.debug "setting state and attribute time to =${curTime}"
state.lPress=curTime
//–end new
//return createEvent([name: ‘lastPress’, value: now(), data:[buttonNumber: button], displayed: false])
return createEvent([name: ‘lastPress’, value: curTime, data:[buttonNumber: button], displayed: false]) //use curTime for debug
}

private createButtonPushedEvent(button) {
log.debug "Button ${button} pushed"
return createEvent([
name: “button”,
value: “pushed”,
data:[buttonNumber: button],
descriptionText: “${device.displayName} button ${button} was pushed”,
isStateChange: true,
displayed: true])
}

private createButtonHeldEvent(button) {
log.debug "Button ${button} held"
return createEvent([
name: “button”,
value: “held”,
data:[buttonNumber: button],
descriptionText: “${device.displayName} button ${button} was held”,
isStateChange: true])
}

private getBatteryLevel(rawValue) {
def intValue = Integer.parseInt(rawValue,16)
def min = 2.1
def max = 3.0
def vBatt = intValue / 10
return ((vBatt - min) / (max - min) * 100) as int
}

def getTemperature(value) {
def celsius = Integer.parseInt(value, 16).shortValue() / 100
if(getTemperatureScale() == “C”){
return celsius
} else {
return celsiusToFahrenheit(celsius) as Integer
}
}

private Map getTemperatureResult(value) {
log.debug 'TEMP’
def linkText = getLinkText(device)
if (tempOffset) {
def offset = tempOffset as int
def v = value as int
value = v + offset
}
def descriptionText = "${linkText} was ${value}°${temperatureScale}"
log.debug "Temp value: "+value
return [
name: ‘temperature’,
value: value,
descriptionText: descriptionText
]
}

// handle commands
def test() {
log.debug “Test”
//zigbee.refreshData(“0”,“4”) + zigbee.refreshData(“0”,“5”) + zigbee.refreshData(“1”,“0x0020”)
zigbee.refreshData(“0x402”,“0”)
}
[/CODE]