zigbee.writeAttribute execution in updated method of device handler

Hi community,
I am writing a device handler for the eCozy zigbee thermostat / TRV and I have the general functionality working (read current temperature, set new heat set points, read current battery status etc.).
Now I want to set some advanced setting in the device and I am pulling my hair out trying to figure out how to change an attribute from the preferences / settings of the device handler.
Below I will post the full device handler code. But first I would like to explain the portion I am stuck on.
I am trying to set the thermostat UI locking capability. All works when I execute a command directly from a tile in the device handler. For that I defined the following:

metadata {
definition (name: “eCozy ZigBee Thermostat”, namespace: “ckpt-martin”, author: “ckpt-martin”) {
capability "Thermostat"
capability "Temperature Measurement"
capability "Battery"
capability "Actuator"
capability "Polling"
capability "Refresh"
capability "Sensor"
capability “Configuration”

    command "quickSetHeat"
    command "increaseHeatSetpoint"
    command "decreaseHeatSetpoint"
    command "parameterSetting"
    command "test_func"

  fingerprint profileId: "0104", endpointId: "03", inClusters: " 0000,0001,0003,000A,0020,0201,0204", outClusters: "0402", manufacturer: "eCozy", model: "Thermostat"

}

standardTile(“test”, “device.test”, decoration: “flat”, width: 2, height: 2) {
state “default”, action:“test_func”, icon:“st.unknown.thing.thing-circle”
}

def test_func() {
delayBetween([
zigbee.writeAttribute(0x204, 0x0001, 0x30, 0x02),
zigbee.readAttribute(0x204, 0x0001),
], 100)
}

I figured out that the thermostat supports the attribute values of 0x00 (no lock), 0x02 (lock temperature setting) and 0x05 (full lock). And when I push the tile define above in my device handler it does set the UI lock.

Now, I want to allow the user to choose between the three options for locking the UI and placed this in the preferences section:

preferences {
input(“lock”, “enum”, title: “Display Lock?”, options: [“No”, “Temperature”, “Touchscreen”], defaultValue: “No”, required: false, displayDuringSetup: false)

}

When I place the writeAttribute statement in the updated method I see from my logging that the preferences setting is accepted and the correct “if” statement is matching, but the lock setting is never changed on the device.

def updated() {
log.debug "updated called"
def lockmode = 0x00
log.info "lock : $settings.lock"
if (settings.lock == “Temperature”) {
log.info "Temperature lock selected"
lockmode = 0x02
}
else if (settings.lock == “Touchscreen”) {
log.info "Touchscreen lock selected"
lockmode = 0x05
}
else {
log.info "No lock selected"
lockmode = 0x00
}
zigbee.writeAttribute(0x204, 0x0001, 0x30, lockmode)
configure()
}

It’s as if the statement can’t be executed and I don’t know why. I have searched the forum and on google and have found device handlers that execute writeAttribute statements within the updated method, so I don’t think it is not allowed at this position. I’m sure I am missing something totally stupid. But the ST documentation is not very detailed with regard to the readAttribute and writeAttribute commands.

Thanks for any help someone can give me to figure this out.

Here’s the full device handler code for reference (please excuse my sloppy coding - I am new to this):

/**

  • Copyright 2017 ckpt-martin
  • 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.
  • eCozy ZigBee Thermostat
  • Author: ckpt-martin
  • Date: 2017-10-30
    */

metadata {
definition (name: “eCozy ZigBee Thermostat”, namespace: “ckpt-martin”, author: “ckpt-martin”) {
capability "Thermostat"
capability "Temperature Measurement"
capability "Battery"
capability "Actuator"
capability "Polling"
capability "Refresh"
capability "Sensor"
capability “Configuration”

    command "quickSetHeat"
    command "increaseHeatSetpoint"
    command "decreaseHeatSetpoint"
    command "parameterSetting"
    command "test_func"

  fingerprint profileId: "0104", endpointId: "03", inClusters: " 0000,0001,0003,000A,0020,0201,0204", outClusters: "0402", manufacturer: "eCozy", model: "Thermostat"

}

preferences {
input(“unitformat”, “enum”, title: “What unit format do you want to display temperature in SmartThings?”, options: [“Celsius”, “Fahrenheit”], defaultValue: “Celsius”, required: false, displayDuringSetup: true)
input(“lock”, “enum”, title: “Display Lock?”, options: [“No”, “Temperature”, “Touchscreen”], defaultValue: “No”, required: false, displayDuringSetup: false)
input(“openwindow”, “enum”, title: “Do you want your thermostat to detect open windows?”, options: [“No”, “Yes”], defaultValue: “No”, required: false, displayDuringSetup: false)
input(“firedetect”, “enum”, title: “Do you want your thermostat to detect fire? (Does not replace fire detectors!)”, options: [“No”, “Yes”], defaultValue: “No”, required: false, displayDuringSetup: false)
}

// simulator metadata
simulator { }

tiles(scale : 2) {
multiAttributeTile(name:“thermostatMulti”, type:“thermostat”, width:6, height:4) {
tileAttribute(“device.temperature”, key: “PRIMARY_CONTROL”) {
attributeState(“temp”, label:’${currentValue}°’)
attributeState(“high”, label:‘HIGH’)
attributeState(“low”, label:‘LOW’)
attributeState("–", label:’–’)
}
tileAttribute(“device.heatingSetpoint”, key: “VALUE_CONTROL”) {
attributeState(“VALUE_UP”, action: “increaseHeatSetpoint”, icon:“st.thermostat.thermostat-up”)
attributeState(“VALUE_DOWN”, action: “decreaseHeatSetpoint”, icon:“st.thermostat.thermostat-down”)
}
tileAttribute(“device.battery”, key: “SECONDARY_CONTROL”) {
attributeState(“battery”, label:’${currentValue}% Battery’, unit:"%", defaultState: true)
}
tileAttribute(“device.thermostatOperatingState”, key: “OPERATING_STATE”) {
attributeState(“idle”, backgroundColor:"#44b621", label:‘Idle’)
attributeState(“heating”, backgroundColor:"#ffa81e", label:‘Heating’)
}
}
standardTile(“configure”, “device.configure”, inactiveLabel: false, decoration: “flat”, width: 2, height: 2) {
state “configure”, label:’’, action:“configuration.configure”, icon:“st.secondary.configure”
}
valueTile(“currentTemp”, “device.temperature”, width: 2, height: 2) {
state “temperature”, label:’${currentValue}°’, icon:“st.alarm.temperature.normal”, canChangeIcon: true, 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”]
]
state “–”, label:’–’, backgroundColor:"#bdbdbd"
}

    standardTile("refresh", "device.refresh", decoration: "flat", width: 2, height: 2) {
        state "default", action:"refresh", icon:"st.secondary.refresh"
    }

    standardTile("test", "device.test", decoration: "flat", width: 2, height: 2) {
        state "default", action:"test_func", icon:"st.unknown.thing.thing-circle"
    }

    main ("currentTemp")
    details(["thermostatMulti", "currentTemp", "refresh", "configure", "test"])
}

}

def test_func() {
delayBetween([
zigbee.writeAttribute(0x204, 0x0001, 0x30, 0x00),
zigbee.readAttribute(0x204, 0x0001),
// zigbee.command(0x201, 0x03),
// zigbee.command(0x201, 0x02),
], 100)
}

// parse events into attributes
def parse(String description) {
log.debug "Parse description $description"
def map = [:]
if (description?.startsWith(“read attr -”)) {
def descMap = parseDescriptionAsMap(description)
log.debug "Desc Map: $descMap"
if (descMap.cluster == “0201” && descMap.attrId == “0000”)
{
log.debug "TEMP: $descMap.value"
map.name = "temperature"
map.value = getTemperature(descMap.value)
if (descMap.value == “7ffd”) //0x7FFD
{
map.value = “low”
}
else if (descMap.value == “7fff”) //0x7FFF
{
map.value = “high”
}
else if (descMap.value == “8000”) //0x8000
{
map.value = “–”
}
sendEvent(name:“temperature”, value:map.value)
}
else if (descMap.cluster == “0001” && descMap.attrId == “0020”)
{
log.debug "BATTERY VOLTAGE: $descMap.value"
map.name = "battery"
map.value = getBatteryPercentage(descMap.value)
if (descMap.value == “24”)
{
map.value = “BATTERY LOW!”
}
sendEvent(name:“battery”, value:map.value)
}

    else if (descMap.cluster == "0201" && descMap.attrId == "0012")
    {
  	log.debug "HEATING SETPOINT: $descMap.value"
  	map.name = "heatingSetpoint"
  	map.value = getTemperature(descMap.value)
        if (descMap.value == "8000")		//0x8000
        {
            map.value = "--"
        }
        sendEvent(name:"heatingSetpoint", value:map.value)
  }
    else if (descMap.cluster == "0201" && descMap.attrId == "0008")
    {
    	log.debug "THERMOSTAT STATE: $descMap.value"
        map.name = "thermostatOperatingState"
        if (descMap.value < "10")
        {
        	map.value = "idle"
        }
        else
        {
        	map.value = "heating"
        }
        sendEvent(name:"thermostatOperatingState", value:map.value)
    }
    else if (descMap.cluster == "0204" && descMap.attrId == "0001")
    {
  	log.debug "LOCK DISPLAY MODE: $descMap.value"
  	map.name = "lockMode"
        if (descMap.value == "00")
        {
  		map.value = "unlocked"
        }
        else if (descMap.value == "02")
        {
  		map.value = "templock"
        }
        else if (descMap.value == "04")
        {
  		map.value = "off"
        }
        else if (descMap.value == "05")
        {
  		map.value = "off"
        }
  }

}

def result = null
if (map) {
result = createEvent(map)
}
log.debug "Parse returned $map"
return result
}

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

def getBatteryPercentage(value) {
if (value != null) {
log.debug(“value $value”)
return Math.round((Integer.parseInt(value, 16) / 30) * 100) // 3.0V = 100%, eCozy uses 2x2AA batteries in parallel
}
}

def getTemperature(value) {
if (value != null) {
log.debug(“value $value”)
def celsius = Integer.parseInt(value, 16) / 100
if (settings.unitformat == “Celsius”) {
return Math.round(celsius)
} else {
return Math.round(celsiusToFahrenheit(celsius))
}
}
}

def quickSetHeat(degrees) {
setHeatingSetpoint(degrees)
}

def setHeatingSetpoint(degrees) {
if (degrees != null) {
def degreesInteger = Math.round(degrees)
float tempfloat;
tempfloat = (Math.round(degrees.toFloat() * 2)) / 2

  log.debug "setHeatingSetpoint({$tempfloat} ${temperatureScale})"
    def celsius = (settings.unitformat == "Celsius") ? tempfloat : (fahrenheitToCelsius(tempfloat) as Float).round(2)
    delayBetween([
    	zigbee.writeAttribute(0x201, 0x12, 0x29, hex(celsius * 100)),
    	zigbee.readAttribute(0x201, 0x12),	//Read Heat Setpoint
        zigbee.readAttribute(0x201, 0x08),	//Read PI Heat demand
	], 100)

}
}

def increaseHeatSetpoint()
{
def currentMode = device.currentState(“thermostatMode”)?.value
if (currentMode != “off”)
{
float currentSetpoint = device.currentValue(“heatingSetpoint”)
float maxSetpoint
float step

	if (settings.unitformat == "Celsius")
	{
    	maxSetpoint = 30;
    	step = 1
	}
	else
	{
    	maxSetpoint = 86
    	step = 1
	}

    if (currentSetpoint < maxSetpoint)
    {
        currentSetpoint = currentSetpoint + step
        quickSetHeat(currentSetpoint)
    }
}

}

def decreaseHeatSetpoint()
{
def currentMode = device.currentState(“thermostatMode”)?.value
if (currentMode != “off”)
{
float currentSetpoint = device.currentValue(“heatingSetpoint”)
float minSetpoint
float step

    if (settings.unitformat == "Celsius")
    {
        minSetpoint = 5;
        step = 1
    }
    else
    {
        minSetpoint = 41
        step = 1
    }

	if (currentSetpoint > minSetpoint)
	{
    	currentSetpoint = currentSetpoint - step
    	quickSetHeat(currentSetpoint)
	}
}

}

def emergencyHeat() {
log.debug "emergencyHeat"
sendEvent(“name”:“thermostatMode”, “value”:“emergency heat”)
“st wattr 0x${device.deviceNetworkId} 0x19 0x201 0x1C 0x30 {05}”
}

def emergencyOff() {
log.debug "emergencyOff"
sendEvent(“name”:“thermostatMode”, “value”:“emergency off”)
“st wattr 0x${device.deviceNetworkId} 0x19 0x201 0x1C 0x30 {05}”
}

def configure() {

log.debug "Configure Reporting"
delayBetween([
//“zdo bind 0x${device.deviceNetworkId} 1 0x19 0x201 {${device.zigbeeId}} {}”,
//Cluster ID (0x0201 = Thermostat Cluster), Attribute ID, Data Type, Payload (Min report, Max report, On change trigger)
zigbee.configureReporting(0x0201, 0x0000, 0x29, 30, 0, 0x0064), //Attribute ID 0x0000 = local temperature, Data Type: S16BIT
zigbee.configureReporting(0x0201, 0x0012, 0x29, 30, 0, 0x0064), //Attribute ID 0x0012 = occupied heat setpoint, Data Type: S16BIT
zigbee.configureReporting(0x0201, 0x001C, 0x30, 1, 0, 1), //Attribute ID 0x001C = system mode, Data Type: 8 bits enum
zigbee.configureReporting(0x0201, 0x0008, 0x20, 300, 7200, 0x05), //Attribute ID 0x0008 = pi heating demand, Data Type: U8BIT

    //Cluster ID (0x0001 = Power)
    zigbee.configureReporting(0x0001, 0x0020, 0x20, 600, 21600, 0x01), 	//Attribute ID 0x0020 = battery voltage, Data Type: U8BIT

    //Cluster ID (0x0204 = Thermostat Ui Conf Cluster), Attribute ID, Data Type, Payload (Min report, Max report, On change trigger)
    //zigbee.configureReporting(0x0204, 0x0000, 0x30, 1, 0, 1),   //Attribute ID 0x0000 = temperature display mode, Data Type: 8 bits enum
	//zigbee.configureReporting(0x0204, 0x0001, 0x30, 1, 0, 1),   //Attribute ID 0x0001 = keypad lockout, Data Type: 8 bits enum   

], 200)

refresh()

}

def refresh() {
delayBetween([
//Read the configured variables
zigbee.readAttribute(0x204, 0x0001), //Read KeypadLockout

    //zigbee.readAttribute(0x000, 0x0000),	//Read ZCLVersion
    //zigbee.readAttribute(0x000, 0x0001),	//Read ApplicationVersion
    //zigbee.readAttribute(0x000, 0x0002),	//Read StackVersion
    //zigbee.readAttribute(0x000, 0x0003),	//Read HWVersion
    //zigbee.readAttribute(0x000, 0x0004),	//Read ManufacturerName
    //zigbee.readAttribute(0x000, 0x0005),	//Read ModelIdentifier
    //zigbee.readAttribute(0x000, 0x0006),	//Read DateCode
    //zigbee.readAttribute(0x000, 0x0007),	//Read PowerSource
    //zigbee.readAttribute(0x000, 0x0010),	//Read LocationDescription
    //zigbee.readAttribute(0x000, 0x0011),	//Read PhysicalEnvironment
    //zigbee.readAttribute(0x000, 0x0012),	//Read DeviceEnabled
    //zigbee.readAttribute(0x000, 0x0014),	//Read DisableLocalConfig

    //zigbee.readAttribute(0x020, 0x0000),	//Read Check-inInterval
    //zigbee.readAttribute(0x020, 0x0001),	//Read LongPollInterval
    //zigbee.readAttribute(0x020, 0x0002),	//Read ShortPollInterval
    //zigbee.readAttribute(0x020, 0x0003),	//Read FastPollTimeout
    //zigbee.readAttribute(0x020, 0x0004),	//Read Check-inIntervalMin
    //zigbee.readAttribute(0x020, 0x0005),	//Read LongPollIntervalMin
    //zigbee.readAttribute(0x020, 0x0006),	//Read FastPollTimeoutMax

    //zigbee.readAttribute(0x00a, 0x0000),	//Read Time
    //zigbee.readAttribute(0x00a, 0x0001),	//Read TimeStatus
    //zigbee.readAttribute(0x00a, 0x0002),	//Read TimeZone
    //zigbee.readAttribute(0x00a, 0x0003),	//Read DstStart
    //zigbee.readAttribute(0x00a, 0x0004),	//Read DstEnd
    //zigbee.readAttribute(0x00a, 0x0005),	//Read DstShift
    //zigbee.readAttribute(0x00a, 0x0006),	//Read StandardTime
    //zigbee.readAttribute(0x00a, 0x0007),	//Read LocalTime
    //zigbee.readAttribute(0x00a, 0x0008),	//Read LastSetTime
    //zigbee.readAttribute(0x00a, 0x0009),	//Read ValidUntilTime

  zigbee.readAttribute(0x001, 0x0020),	//Read BatteryVoltage
  //zigbee.readAttribute(0x001, 0x0030),	//Read BatteryManufacturer
  //zigbee.readAttribute(0x001, 0x0031),	//Read BatterySize
  //zigbee.readAttribute(0x001, 0x0032),	//Read BatteryAHrRating
  //zigbee.readAttribute(0x001, 0x0033),	//Read BatteryQuantity
  //zigbee.readAttribute(0x001, 0x0034),	//Read BatteryRatedVoltage
  //zigbee.readAttribute(0x001, 0x0035),	//Read BatteryAlarmMask
  //zigbee.readAttribute(0x001, 0x0036),	//Read BatteryVoltageMinThreshold

  zigbee.readAttribute(0x201, 0x0000),	//Read LocalTemperature
  //zigbee.readAttribute(0x201, 0x0003),	//Read AbsMinHeatSetpointLimit
  //zigbee.readAttribute(0x201, 0x0004),	//Read AbsMaxHeatSetpointLimit
  zigbee.readAttribute(0x201, 0x0008),	//Read PIHeatingDemand
  //zigbee.readAttribute(0x201, 0x0010),	//Read LocalTemperatureCalibration
	zigbee.readAttribute(0x201, 0x0012),	//Read OccupiedHeatingSetpoint
	//zigbee.readAttribute(0x201, 0x0015),	//Read MinHeatSetpointLimit
	//zigbee.readAttribute(0x201, 0x0016),	//Read MaxHeatSetpointLimit
	//zigbee.readAttribute(0x201, 0x0019),	//Read MinSetpointDeadBand
	//zigbee.readAttribute(0x201, 0x001A),	//Read RemoteSensing
	//zigbee.readAttribute(0x201, 0x001B),	//Read ControlSequenceOfOperation
    zigbee.readAttribute(0x201, 0x001C),	//Read SystemMode
    zigbee.readAttribute(0x201, 0x001E),	//Read ThermostatRunningMode
    //zigbee.readAttribute(0x201, 0x0020),	//Read StartOfWeek
    //zigbee.readAttribute(0x201, 0x0021),	//Read NumberOfWeeklyTransitions
    //zigbee.readAttribute(0x201, 0x0022),	//Read NumberOfDailyTransitions
    //zigbee.readAttribute(0x201, 0x0023),	//Read TemperatureSetpointHold
    zigbee.readAttribute(0x201, 0x0030),	//Read SetpointChangeSource
    //zigbee.readAttribute(0x201, 0x0031),	//Read SetpointChangeAmount
    zigbee.readAttribute(0x201, 0x0032),	//Read SetpointChangeSourceTimestamp

    //zigbee.readAttribute(0x204, 0x0000),	//Read TemperatureDisplayMode

], 100)
}

def updated() {
log.debug "updated called"
def lockmode = 0x00
log.info "lock : $settings.lock"
if (settings.lock == “Temperature”) {
log.info "Temperature lock selected"
lockmode = 0x02
}
else if (settings.lock == “Touchscreen”) {
log.info "Touchscreen lock selected"
lockmode = 0x05
}
else {
log.info "No lock selected"
lockmode = 0x00
}
zigbee.writeAttribute(0x204, 0x0001, 0x30, lockmode)

    configure()

}

private hex(value) {
new BigInteger(Math.round(value).toString()).toString(16)
}

private getEndpointId() {
new BigInteger(device.endpointId, 16).toString()
}

private String swapEndianHex(String hex) {
reverseArray(hex.decodeHex()).encodeHex()
}

private byte[] reverseArray(byte[] array) {
int i = 0;
int j = array.length - 1;
byte tmp;
while (j > i) {
tmp = array[j];
array[j] = array[i];
array[i] = tmp;
j–;
i++;
}
return array
}

Write it as a HubAction. (note syntax might not be accurate)

new physicalgraph.device.HubAction( zigbee.writeAttribute(0x204, 0x0001, 0x30, lockmode) )

Thanks.
Looks like the syntax would be:
sendHubCommand(physicalgraph.device.HubAction(“zigbee.writeAttribute(0x204, 0x0001, 0x30, lockmode)”))

But, now I get a Java error:
java.lang.NullPointerException: Cannot get property ‘device’ on null object

Sorry, wrong syntax. It should have been:
sendHubCommand(new physicalgraph.device.HubAction(“zigbee.writeAttribute(0x204, 0x0001, 0x30, $lockmode)”))

Although this gets rid of the java error, it still has no effect just like my other attempts without the HubAction. :frowning:

OK, thanks to your help and searching some more based off of that info I finally figured it out. I don’t quite understand why ST device handlers are so finicky. I can run the zigbee.writeAttribute directly within methods I define and fire via a tile action. But for the pre-defined methods like the updated method I had to do this:

updated() {
.....
def cmds = 
	zigbee.writeAttribute(0x204, 0x0001, 0x30, lockmode) +
	zigbee.readAttribute(0x204, 0x0001)
log.info "updated() --- cmds: $cmds"
fireCommand(cmds)
....
}

private fireCommand(List commands) {
	if (commands != null && commands.size() > 0) {
		log.trace("Executing commands:" + commands)
		for (String value : commands){
			sendHubCommand([value].collect {new physicalgraph.device.HubAction(it)})
		}
	}
}

This finally allowed me to actually write

I’m glad you figured it out! There is certianly a distinction on how commands work when called from the parse() function versus called from a tile. I ran into this on a Iris Z-Wave Repeater DTH I wrote for myself. Apologies, I should have given you the complete code, I was in a hurry to get to work! :slight_smile: