Custom Attribute Problems

I have a device driver for a Zigbee temperature and humidity measurement device I built. I am trying to set up the device driver. I created the driver and everything works showing temperature in Celsius. I want to show both Celsius and Farenheit on the multi-attribute tile so I added a custom attribute called tempf. My device handler correctly processes the data and I can see the attributes against the device but the multi-attribute tile shows the temperature attribute (Celsius) in both locations even though the secondary location is set to show tempf. When the device updates the temperature, if the device is showing in the UI (iPhone) the secondary attribute changes to show the correct Farenheit reading but if I switch to another device then back again it is back to showing the main temperature attribute again.

I tried adding another tile specifically for the tempf attribute and got the same results. Showing Celsius until the first update happens then showing Farenheit until closed and reopened. It seems like some sort of display initialisation problem. Here is my device handler.
Does anybody have any ideas?

/**
 *  NordicMultiSensor
 *
 *  Copyright 2019 Ian Abercrombie
 *
 *  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 physicalgraph.zigbee.zcl.DataType
private getCLUSTER_POWER() { 0x0001 }
private getPOWER_ATTR_BATTERY_VOLTAGE() { 0x0020 }
private getPOWER_ATTR_BATTERY_REMAINING() { 0x0021 }
private getCLUSTER_TEMPERATURE_MEASUREMENT() { 0x0402 }
private getCLUSTER_RELATIVE_HUMIDITY_MEASUREMENT() { 0x0405 }
private getATTR_MEASUREDVALUE() { 0x0000 }
private getResourcesUrl() { return "https://raw.githubusercontent.com/krlaframboise/Resources/master/Zooz/" }

metadata {
	definition (name: "NordicMultiSensor", namespace: "Cedar", author: "Ian Abercrombie") {
		capability "Temperature Measurement"
        capability "Relative Humidity Measurement"
        capability "Sensor"
        capability "Battery"
        attribute "tempf","number"

		command "refresh"
        
    	fingerprint profileId: "0104", deviceId: "0304", inClusters: "0000,0003,0402,0405", outClusters: "0003", manufacturer: "Cedar Technology", model: "NordicMultiSensor", deviceJoinName: "Nordic Multi Sensor"
	}

	tiles(scale: 2) {
	    multiAttributeTile(name:"temperatures", type:"generic", width:6, height:4) {
            tileAttribute("device.temperature", key: "PRIMARY_CONTROL") {
                attributeState("temperature", label:'${currentValue}°C', icon: "${resourcesUrl}temperature.png",
                    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"]])
            }
            tileAttribute("tempf", key: "SECONDARY_CONTROL") {
                attributeState("tempf", label:'${currentValue}°F')
            }
        }
        valueTile("temperature2", "device.temperature", inactiveLabel: false) {
	        state "temperature", label:'${currentValue}°', icon:"st.Weather.weather2",
                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"]]}

//		valueTile("temperature", "device.temperature", inactiveLabel: false, width: 2, height: 2) {
//       	    state "temperature", label:'${currentValue}°C', icon:"st.Weather.weather2"
//        }
        valueTile("humidity", "device.humidity", inactiveLabel: false, width: 2, height: 2) {
       	    state "humidity", label:'${currentValue}%', unit:"", icon: "${resourcesUrl}humidity.png"
		        backgroundColors:[
        	        [value: 0, color: "#FFFCDF"],
            	    [value: 4, color: "#FDF789"],
                	[value: 20, color: "#A5CF63"],
                	[value: 23, color: "#6FBD7F"],
                	[value: 56, color: "#4CA98C"],
                	[value: 59, color: "#0072BB"],
                	[value: 76, color: "#085396"]]
        }
		valueTile("batteryVoltage", "device.batteryVoltage", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
       		state "batteryVoltage", label:'${currentValue}V', unit: "", icon: "${resourcesUrl}battery.png"
        }
        
            
		valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
			state "battery", label:'${currentValue}%', unit: "", icon: "${resourcesUrl}battery.png"
       	}

		standardTile("refresh", "device.temperature", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
			state "default", label:'refresh', action:"refresh"
		}
        standardTile("tempf", "device.tempf", inactiveLabel: false, width: 2, height: 2) {
        	state "tempf", label:'${currentValue}°F', unit:"F", icon: "${resourcesUrl}temperature.png"
        }
        standardTile("identify", "device.temperature", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
			state "default", label:'identify', action:"identify"
        }
       	main ("temperatures")
		details(["temperatures","humidity","batteryVoltage","battery","refresh","tempf","identify"])
	}
}

private Map parseCatchAllMessage(String description) {
    Map resultMap = [:]
    def cluster = zigbee.parse(description)

    switch(cluster.clusterId) {
        case 0x0402:	// Temperature cluster
            switch(cluster.command) {
                case 0x07: log.debug "Configure temperature reporting response = ${cluster.data}"
                return resultMap
                break
            }
            log.debug "Parse temperature cluster data : ${cluster}"
            break
        case 0x0405:    // Humidity cluster
            switch(cluster.command) {
                case 0x07: log.debug "Configure humidity reporting response = ${cluster.data}"
                return resultMap
                break
            }
        case 0x0001:	// Power cluster
            switch(cluster.command) {
                case 0x07: log.debug "Configure power reporting response = ${cluster.data}"
                return resultMap
                break
            }
            log.debug "Parse power cluster data : ${cluster}"
            break        
        case 0x8021:	//Bind Response
            log.debug "Bind response from NordicMultiSensor : ${cluster.command}"
            break
        default : // Unknown
	    log.debug "Description = '${description}' ; Parse returned ${cluster}"
    }
    return zigbee.getEvent(description)
}

private Map parseReadAttr(String description) {
    Map resultMap = [:]
    description.split(",").each { param ->
        def pair = param.split(":")
    	resultMap[pair[0].trim()] = pair[1].trim()
    }
    return resultMap
}

// parse events into attributes
def parse(String description) {
    Map map = [:]
    log.debug "parsing : ${description}"
    if (description?.startsWith('catchall:')) {
	map = parseCatchAllMessage(description)
        if (map[name] == 'temperature') {
            log.debug "Setting temperature - ${map}"
            createEvent(map)
            
	} else {
            log.debug "parse : ${description}"
        }
    } else {
        def pair = description.split(":")
        def name = pair[0].trim()
        def value = pair[1].trim()
        
        switch(name) {
            case 'temperature' : 
		        def farenheit = (((value.toFloat() / 5) * 9) + 32).round(2)
            	log.debug "Got temperature - ${description} - creating events ${name} : ${value} and farenheit : ${farenheit}"
                def tempCelsiusEvent = createEvent(name: name, value: value, unit:"C", isStateChange: true)
                def tempFarenheitEvent = createEvent(name: "tempf", value: farenheit, unit:"F", isStateChange: true)
	        	return [tempCelsiusEvent, tempFarenheitEvent]
                break
            case 'humidity' : 
            	log.debug "Got humidity - ${description} - creating event ${name} : ${value}"
            	return createEvent(name: name, value: value, unit:"%")
                break
            case 'read attr - raw' : log.debug "Read attr raw"
            	map = parseReadAttr(description)
                log.debug "Attribute = ${map.attrId}"
                switch(map["attrId"]) {
                    case "0020" : // Voltage
	                float v = Integer.parseInt(map["value"],16) / 10;
                    	log.debug "Voltage updated ${v}"
			return createEvent(name: "batteryVoltage", value: v, unit:"V") 
                    	break
                    case "0021" : // Percent Remaining
	                float r = Integer.parseInt(map["value"],16) / 2;
                        log.debug "Percent Remaining updated ${r}"
			return createEvent(name: "battery", value: r, unit:"%") 
                    	break
                    default : log.debug "Unknown attribute - ${map["attrId"]}"
                }
            	break;
            default : log.debug "Parsing '${description}'"
        }
    }
    return map
}


def configure() {
    // 	reportableChange -------------------------------------------------------------------------------------------
    //  maxReportTime -----------------------------------------------------------------------------------------     |
    //  minReportTime ------------------------------------------------------------------------------------     |    |
    //  dataType ------------------------------------------------------------------------------------     |    |    |
    //  attribute -----------------------------------------------------------                        |    |    |    |
    //  cluster --------------------------------------                       |                       |    |    |    |
    //                                                |                      |                       |    |    |    |
    return zigbee.configureReporting(CLUSTER_TEMPERATURE_MEASUREMENT, ATTR_MEASUREDVALUE, DataType.INT16, 5, 3600, 10) + 
            zigbee.configureReporting(CLUSTER_RELATIVE_HUMIDITY_MEASUREMENT, ATTR_MEASUREDVALUE, DataType.UINT16, 5, 3600, 50) +
    	    zigbee.configureReporting(CLUSTER_POWER, POWER_ATTR_BATTERY_VOLTAGE, DataType.UINT8, 5, 3600, 1) +
            zigbee.configureReporting(CLUSTER_POWER, POWER_ATTR_BATTERY_REMAINING, DataType.UINT8, 5, 3600, 1)
}

def refresh() {
	log.debug "refresh, configuring reporting."
	return configure()
}

def installed() {
	initialize()
}

def updated() {
	initialize()
}

def initialize() {
	log.debug "Initialize device. Configuring..."
	return configure()
}

def setLevel(value, rate = null) {
    setTemperature(value)
}

def up() {
    setTemperature(getTemperature() + 1)
    log.debug "Temperature increased"
}

def down() {
    setTemperature(getTemperature() - 1)
    log.debug "Temperature decreased"

}

def setTemperature(value) {
    sendEvent(name:"temperature", value: value)
}

private getTemperature() {
    def ts = device.currentState("temperature")
    Integer value = ts ? ts.integerValue : 72
    return value
}

I’d be expecting to see tileAttribute("device.tempf", key: "SECONDARY_CONTROL") rather than just referencing ‘tempf’, and for the separate tile I’d expect to see a valueTile rather than a standardTile as ‘tempf’ is not a state value. However whether any of that makes any difference or turns out to be cosmetic is another matter.

Many thanks orangebucket but unfortunately your fear that these changes would be cosmetic turns out to be valid. In fact I am pretty sure I started out defining them the way you suggested but I have changed so many things trying to get this to work I have lost track somewhat. I made the changes you suggested but of course it still does the same thing. One thing I notice is that on the main tile which is a multi-attribute tile, the temperature is displayed as a floating point showing the true value provided. When the secondary attribute (tempf) is displayed initially in the bottom left of the tile it shows the temerature instead of tempf attribute but it also rounds it to an integer value. The same behaviour is observed in the value tile assigned to the tempf attribute. As soon as the temf is updated by the sensor whilst the UI is showing, the value changes to a float with two decimal places showing the correct attribute value.

I found my problem.
In the create event code I am specifying the units as C for Celsius and F for Farenheit. In my SmartThings app I have the temperature unit set to C. This is causing the app to convert the data sent in F to C rounding to the nearest integer. It seems that there is a bug in the UI such that when the updates happen they no longer convert according to the settings in the app but that is OK for me since I didn’t want it to do that in the first place.
To fix my problem I simply removed the unit: definition in the create event map. SmartThings no longer sees the unit as C or F and does not attempt to convert. I am free to display both on the same tile making it easy to see the temperature in both units at the same time.

© 2019 SmartThings, Inc. All Rights Reserved. Terms of Use | Privacy Policy

SmartThings; SmartApps®; Physical Graph; Hello, Home; and Hello, Smart Home are all trademarks of the SmartThings, Inc.