[BETA] Hue Motion Sensor (beta) (no hue bridge)

THANK YOU VERY MUCH JANW IT’S WORKING WITH ST NOW THANKS TO YOU :thumbsup:… THE ALL CAPS, I WILL TAKE IT INTO CONSIDERATION :relaxed:… MERRY CHRISTMAS :gift:

@kunid I get the exact same issues with my setup (Hue Sensor via DTH and not making hue bulb come back on straight away if motion as soon as hue bulb goes off).

Anyone else see the same behaviour?

Does anyone know what the ‘Configure’ button does when in the app?

I checked out the lux reading with my lux meter and they are off. My lux meter reads around 88 while the motion sensor reports around 57

If you want to calibrate I could set up a more controlled test.

Have you had any luck with this issue ?

Can anyone else assist us with this - is this expected behaviour?

@Townsmcp, @kunid, and anyone else having issues with the sensor taking too long to reset…

Locate the following line (should be line 152):

configCmds += zigbee.configureReporting(0x406,0x0000, 0x18, 30, 600, null) // motion // confirmed

The “30, 600” is the minimum and maximum reporting time in seconds. Change the “30” to something like “10” or even “5”, and it will reset much faster. I went down to 5 seconds as a test with no noticeable effect on network speed or latency.

You may even be able to go to 1 second, although I don’t see the need to go much lower than 1/2 of your desired delay time…assuming you are using something like CoRE to control the sensor. As long as the DTH requests another update before or around the time your coded timer has expired, it should reset.

This DTH is now usable for me, but YMMV.

Your work is much appreciated @Stephen_McLaughlin!

2 Likes

I hope @Stephen_McLaughlin doesn’t mind, but I modified the DTH to include an offset for the luminance measurement in the preferences. This was down and dirty and there is no error checking or anything, so use at your own risk. The extent of my testing was to try different luminance offsets and verify that my CoRE piston triggered the motion sensor as expected when above or below certain thresholds.

NOTE: I’m just a hack in the process of learning how ST programming works, and did this for my own amusement. I accept no responsibility for property damage or missing children. :grin:

/**
 *  Copyright 2015 SmartThings
 *
 *  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: "Hue Motion Sensor", namespace: "digitalgecko", author: "digitalgecko") {

        
		capability "Motion Sensor"
		capability "Configuration"
		capability "Battery"
		capability "Refresh"
        capability "Temperature Measurement"
        capability "Sensor"
        capability "Illuminance Measurement" //0x0400

        fingerprint profileId: "0104", inClusters: "0000,0001,0003,0406,0400,0402", outClusters: "0019", manufacturer: "Philips", model: "SML001", deviceJoinName: "Hue Motion Sensor"
    }

	preferences {
    		section {
			input title: "Temperature Offset", description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter '-5'. If 3 degrees too cold, enter '+3'.", displayDuringSetup: false, type: "paragraph", element: "paragraph"
			input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
		}
            section {
			input title: "Luminance Offset", description: "This feature allows you to correct the luminance reading by selecting an offset. Enter a value such as 20 or -20 to adjust the luminance reading.", displayDuringSetup: false, type: "paragraph", element: "paragraph"
			input "luxOffset", "number", title: "Lux", description: "Adjust luminance by this amount", range: "*..*", displayDuringSetup: false
		}
    }

    tiles(scale: 2) {
		multiAttributeTile(name:"motion", type: "generic", width: 6, height: 4){
			tileAttribute ("device.motion", key: "PRIMARY_CONTROL") {
				attributeState "active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#53a7c0"
				attributeState "inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff"
			}
		}
       
        
		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"]
				]
			)
		}
        
        valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) {
			state "battery", label:'${currentValue}% battery', unit:""
		}
		valueTile("illuminance", "device.illuminance", width: 2, height: 2) {
			state("illuminance", label:'${currentValue}', unit:"lux",
				backgroundColors:[
					[value: 9, color: "#767676"],
					[value: 315, color: "#ffa81e"],
					[value: 1000, color: "#fbd41b"]
				]
			)
		}
        standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
            state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
        }
        standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
            state "default", label:"configure", action:"configure"
        }
        main "motion"
        details(["motion","temperature","battery", "refresh","illuminance",'configure'])
    }
}



// Parse incoming device messages to generate events
def parse(String description) {
    def msg = zigbee.parse(description)
    
    //log.warn "--"
    //log.trace description
    //log.debug msg
    //def x = zigbee.parseDescriptionAsMap( description )
    //log.error x
    
	Map map = [:]
    if (description?.startsWith('catchall:')) {
		map = parseCatchAllMessage(description)
	}
	else if (description?.startsWith('temperature: ')) {
		map = parseCustomMessage(description)
	}
    else if (description?.startsWith('illuminance: ')) {
		map = parseCustomMessage(description)
	}
//	else if (description?.startsWith('zone status')) {
//		//map = parseIasMessage(description)
//        log.trace "zone status"
//	}

	def result = map ? createEvent(map) : null

	if (description?.startsWith('enroll request')) {
		List cmds = enrollResponse()
		result = cmds?.collect { new physicalgraph.device.HubAction(it) }
	}
	else if (description?.startsWith('read attr -')) {
		result = parseReportAttributeMessage(description).each { createEvent(it) }
	}
	return result
}

/*
  Refresh Function
*/
def refresh() {
    log.debug "Refreshing Values"

    def refreshCmds = []
    refreshCmds +=zigbee.readAttribute(0x0001, 0x0020) // Read battery?
    refreshCmds += zigbee.readAttribute(0x0402, 0x0000) // Read temp?
    refreshCmds += zigbee.readAttribute(0x0400, 0x0000) // Read luminance?
    refreshCmds += zigbee.readAttribute(0x0406, 0x0000) // Read motion?

    return refreshCmds + enrollResponse()

    }
/*
  Configure Function
*/
def configure() {

// TODO : device watch?

	String zigbeeId = swapEndianHex(device.hub.zigbeeId)
	log.debug "Confuguring Reporting and Bindings."
    
    
	def configCmds = []
    configCmds += zigbee.batteryConfig()
	configCmds += zigbee.temperatureConfig(30, 600) // Set temp reporting times // Confirmed
    
    configCmds += zigbee.configureReporting(0x406,0x0000, 0x18, 10, 600, null) // motion // confirmed
    
    
    // Data type is not 0x20 = 0x8D invalid data type Unsigned 8-bit integer
    
	configCmds += zigbee.configureReporting(0x400,0x0000, 0x21, 60, 600, 0x20) // Set luminance reporting times?? maybe    
    return refresh() + configCmds 
}

/*
	getMotionResult
 */

private Map getMotionResult(value) {
    //log.trace "Motion : " + value
	
    def descriptionText = value == "01" ? '{{ device.displayName }} detected motion':
			'{{ device.displayName }} stopped detecting motion'
    
    return [
		name: 'motion',
		value: value == "01" ? "active" : "inactive",
		descriptionText: descriptionText,
		translatable: true,
	]
}


/*
  getTemperatureResult
*/
private Map getTemperatureResult(value) {

	//log.trace "Temperature : " + value
	if (tempOffset) {
		def offset = tempOffset as int
		def v = value as int
		value = v + offset
	}
	def descriptionText = temperatureScale == 'C' ? '{{ device.displayName }} was {{ value }}°C':
			'{{ device.displayName }} was {{ value }}°F'

	return [
		name: 'temperature',
		value: value,
		descriptionText: descriptionText,
		translatable: true,
		unit: temperatureScale
	]
}

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

private Map getLuminanceResult(rawValue) {
	log.debug "Luminance rawValue = ${rawValue}"

	def result = [
		name: 'illuminance',
		value: '--',
		translatable: true,
 		unit: 'lux'
	]
    
    result.value = rawValue + luxOffset as Integer
    return result
}

/*
	getBatteryResult
*/
//TODO: needs calibration
private Map getBatteryResult(rawValue) {
	//log.debug "Battery rawValue = ${rawValue}"

	def result = [
		name: 'battery',
		value: '--',
		translatable: true
	]

	def volts = rawValue / 10

	if (rawValue == 0 || rawValue == 255) {}
	else {
		if (volts > 3.5) {
			result.descriptionText = "{{ device.displayName }} battery has too much power: (> 3.5) volts."
		}
		else {
			if (device.getDataValue("manufacturer") == "SmartThings") {
				volts = rawValue // For the batteryMap to work the key needs to be an int
				def batteryMap = [28:100, 27:100, 26:100, 25:90, 24:90, 23:70,
								  22:70, 21:50, 20:50, 19:30, 18:30, 17:15, 16:1, 15:0]
				def minVolts = 15
				def maxVolts = 28

				if (volts < minVolts)
					volts = minVolts
				else if (volts > maxVolts)
					volts = maxVolts
				def pct = batteryMap[volts]
				if (pct != null) {
					result.value = pct
					result.descriptionText = "{{ device.displayName }} battery was {{ value }}%"
				}
			}
			else {
				def minVolts = 2.1
				def maxVolts = 3.0
				def pct = (volts - minVolts) / (maxVolts - minVolts)
				def roundedPct = Math.round(pct * 100)
				if (roundedPct <= 0)
					roundedPct = 1
				result.value = Math.min(100, roundedPct)
				result.descriptionText = "{{ device.displayName }} battery was {{ value }}%"
			}
		}
	}

	return result
}
/*
	parseCustomMessage
*/
private Map parseCustomMessage(String description) {
	Map resultMap = [:]
	if (description?.startsWith('temperature: ')) {
		def value = zigbee.parseHATemperatureValue(description, "temperature: ", getTemperatureScale())
		resultMap = getTemperatureResult(value)
	}
    
    if (description?.startsWith('illuminance: ')) {
    log.warn "value: " + description.split(": ")[1]
            log.warn "proc: " + value

		def value = zigbee.lux( description.split(": ")[1] as Integer ) //zigbee.parseHAIlluminanceValue(description, "illuminance: ", getTemperatureScale())
		resultMap = getLuminanceResult(value)
	}
	return resultMap
}

/*
	parseReportAttributeMessage
*/
private List parseReportAttributeMessage(String description) {
	Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param ->
		def nameAndValue = param.split(":")
		map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
	}

	List result = []
    
    // Temperature
	if (descMap.cluster == "0402" && descMap.attrId == "0000") {
		def value = getTemperature(descMap.value)
		result << getTemperatureResult(value)
	}
    
    // Motion
   	else if (descMap.cluster == "0406" && descMap.attrId == "0000") {
    	result << getMotionResult(descMap.value)
	}
    
    // Battery
	else if (descMap.cluster == "0001" && descMap.attrId == "0020") {
		result << getBatteryResult(Integer.parseInt(descMap.value, 16))
	}
    
    // Luminance
    else if (descMap.cluster == "0402" ) { //&& descMap.attrId == "0020") {
		log.error "Luminance Response " + description
        //result << getBatteryResult(Integer.parseInt(descMap.value, 16))
	}

	return result
}


/*
	parseCatchAllMessage
*/
private Map parseCatchAllMessage(String description) {
	Map resultMap = [:]
	def cluster = zigbee.parse(description)
//	log.debug cluster
	if (shouldProcessMessage(cluster)) {
		switch(cluster.clusterId) {
			case 0x0001:
				// 0x07 - configure reporting
				if (cluster.command != 0x07) {
					resultMap = getBatteryResult(cluster.data.last())
				}
			break

			case 0x0400:
            	if (cluster.command == 0x07) { // Ignore Configure Reporting Response
                	if(cluster.data[0] == 0x00) {
						log.trace "Luminance Reporting Configured"
						sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
					}
					else {
						log.warn "Luminance REPORTING CONFIG FAILED- error code:${cluster.data[0]}"
					}
				}
				else {
            		log.debug "catchall : luminance" + cluster
                	resultMap = getLuminanceResult(cluster.data.last());
                }

			break
            
			
            
			case 0x0402:
				if (cluster.command == 0x07) {
					if(cluster.data[0] == 0x00) {
						log.trace "Temperature Reporting Configured"
						sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
					}
					else {
						log.warn "TEMP REPORTING CONFIG FAILED- error code:${cluster.data[0]}"
					}
				}
				else {
					// temp is last 2 data values. reverse to swap endian
					String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
					def value = getTemperature(temp)
					resultMap = getTemperatureResult(value)
				}
			break
		}
	}

	return resultMap
}

private boolean shouldProcessMessage(cluster) {
	// 0x0B is default response indicating message got through
	boolean ignoredMessage = cluster.profileId != 0x0104 ||
	cluster.command == 0x0B ||
	(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
	return !ignoredMessage
}


// This seems to be IAS Specific and not needed we are not really a motion sensor
def enrollResponse() {
//	log.debug "Sending enroll response"
//	String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
//	[
//		//Resending the CIE in case the enroll request is sent before CIE is written
//		"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
//		"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
//		//Enroll Response
//		"raw 0x500 {01 23 00 00 00}", "delay 200",
//		"send 0x${device.deviceNetworkId} 1 1", "delay 200"
//	]
}

def configureHealthCheck() {
    Integer hcIntervalMinutes = 12
    refresh()
    sendEvent(name: "checkInterval", value: hcIntervalMinutes * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
}

def updated() {
    log.debug "in updated()"
    configureHealthCheck()
}

def ping() {
    return zigbee.onOffRefresh()
}





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
}
3 Likes

Thanks @calansvc. I will give the new reporting time a try. I am also going to try out your luminance offset

I just added another sensor, and the Lux capability didn’t show up until I entered 0 for the offset in the preferences. Looks like I need to figure out how to add a default value for the offset in the DTH. :slight_smile:

hehe - that is just what I was going to report. I have tested out a dining room sensor but the light wouldn’t go on (lux is effectively 0 however my CoRE piston says turn on light below 15) and had the same thing - no change in lux level. I even tried resetting and then deleting the sensor and re-adding it. While that was going on I tried another room sensor - light on and no change to lux so I changed the offset to 0 also. Light on and off a couple of times for that room and then the lux levels started coming through.
Unfortunately, even though I have set the lux offset to 0 for dining room I still have “–” as the lux level on a newly joined motion sensor :frowning: Great try though

Try setting the offset to 1 and see if it starts reporting.

You might also need to set the offset to 0 (or whatever) during the process of adding the sensor, when it asks you to name it.

I’ll learn a bit more about groovy and these DTH’s and see if I can make this work properly, unless the author revisits this and fixes it up for us. I was just experimenting and trying stuff.

1 Like

One thing I’ve noticed with these sensors (even without my added offset), is that the lux value needs to be at least 10 or 15 below the threshold, and it seems that the sensor sees “spikes” in the values that aren’t visible to us. I had a light that kept turning on even though the sensor always showed it was 5 to 10 above the lux threshold…but once I dropped the threshold to be about 15 below the current reading, it started working properly.

I also noticed that just walking into the room would cause one of those minor fluctuations in lux, and could throw off the threshold rule. Moral of the story, give yourself a good cushion between the actual lux value and the threshold you want to trigger at.

This is all really good work @calansvc - well done!

@Townsmcp

I think I’ve fixed the issue with the default luminance offset. I think. :slight_smile:

/**
 *  Copyright 2015 SmartThings
 *
 *  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: "Hue Motion Sensor", namespace: "digitalgecko", author: "digitalgecko") {

        
		capability "Motion Sensor"
		capability "Configuration"
		capability "Battery"
		capability "Refresh"
        capability "Temperature Measurement"
        capability "Sensor"
        capability "Illuminance Measurement" //0x0400

        fingerprint profileId: "0104", inClusters: "0000,0001,0003,0406,0400,0402", outClusters: "0019", manufacturer: "Philips", model: "SML001", deviceJoinName: "Hue Motion Sensor"
    }

	preferences {
    		section {
			input title: "Temperature Offset", description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter '-5'. If 3 degrees too cold, enter '+3'.", displayDuringSetup: false, type: "paragraph", element: "paragraph"
			input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
		}
            section {
			input title: "Luminance Offset", description: "This feature allows you to correct the luminance reading by selecting an offset. Enter a value such as 20 or -20 to adjust the luminance reading.", displayDuringSetup: false, type: "paragraph", element: "paragraph"
			input "luxOffset", "number", title: "Lux", description: "Adjust luminance by this amount", range: "*..*", displayDuringSetup: false
		}
    }

    tiles(scale: 2) {
		multiAttributeTile(name:"motion", type: "generic", width: 6, height: 4){
			tileAttribute ("device.motion", key: "PRIMARY_CONTROL") {
				attributeState "active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#53a7c0"
				attributeState "inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff"
			}
		}
       
        
		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"]
				]
			)
		}
        
        valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) {
			state "battery", label:'${currentValue}% battery', unit:""
		}
		valueTile("illuminance", "device.illuminance", width: 2, height: 2) {
			state("illuminance", label:'${currentValue}', unit:"lux",
				backgroundColors:[
					[value: 9, color: "#767676"],
					[value: 315, color: "#ffa81e"],
					[value: 1000, color: "#fbd41b"]
				]
			)
		}
        standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
            state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
        }
        standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
            state "default", label:"configure", action:"configure"
        }
        main "motion"
        details(["motion","temperature","battery", "refresh","illuminance",'configure'])
    }
}



// Parse incoming device messages to generate events
def parse(String description) {
    def msg = zigbee.parse(description)
    
    //log.warn "--"
    //log.trace description
    //log.debug msg
    //def x = zigbee.parseDescriptionAsMap( description )
    //log.error x
    
	Map map = [:]
    if (description?.startsWith('catchall:')) {
		map = parseCatchAllMessage(description)
	}
	else if (description?.startsWith('temperature: ')) {
		map = parseCustomMessage(description)
	}
    else if (description?.startsWith('illuminance: ')) {
		map = parseCustomMessage(description)
	}
//	else if (description?.startsWith('zone status')) {
//		//map = parseIasMessage(description)
//        log.trace "zone status"
//	}

	def result = map ? createEvent(map) : null

	if (description?.startsWith('enroll request')) {
		List cmds = enrollResponse()
		result = cmds?.collect { new physicalgraph.device.HubAction(it) }
	}
	else if (description?.startsWith('read attr -')) {
		result = parseReportAttributeMessage(description).each { createEvent(it) }
	}
	return result
}

/*
  Refresh Function
*/
def refresh() {
    log.debug "Refreshing Values"

    def refreshCmds = []
    refreshCmds +=zigbee.readAttribute(0x0001, 0x0020) // Read battery?
    refreshCmds += zigbee.readAttribute(0x0402, 0x0000) // Read temp?
    refreshCmds += zigbee.readAttribute(0x0400, 0x0000) // Read luminance?
    refreshCmds += zigbee.readAttribute(0x0406, 0x0000) // Read motion?

    return refreshCmds + enrollResponse()

    }
/*
  Configure Function
*/
def configure() {

// TODO : device watch?

	String zigbeeId = swapEndianHex(device.hub.zigbeeId)
	log.debug "Confuguring Reporting and Bindings."
    
    
	def configCmds = []
    configCmds += zigbee.batteryConfig()
	configCmds += zigbee.temperatureConfig(60, 600) // Set temp reporting times // Confirmed
    
    configCmds += zigbee.configureReporting(0x406,0x0000, 0x18, 30, 600, null) // motion // confirmed
    
    
    // Data type is not 0x20 = 0x8D invalid data type Unsigned 8-bit integer
    
	configCmds += zigbee.configureReporting(0x400,0x0000, 0x21, 60, 600, 0x20) // Set luminance reporting times?? maybe    
    return refresh() + configCmds 
}

/*
	getMotionResult
 */

private Map getMotionResult(value) {
    //log.trace "Motion : " + value
	
    def descriptionText = value == "01" ? '{{ device.displayName }} detected motion':
			'{{ device.displayName }} stopped detecting motion'
    
    return [
		name: 'motion',
		value: value == "01" ? "active" : "inactive",
		descriptionText: descriptionText,
		translatable: true,
	]
}


/*
  getTemperatureResult
*/
private Map getTemperatureResult(value) {

	//log.trace "Temperature : " + value
	if (tempOffset) {
		def offset = tempOffset as int
		def v = value as int
		value = v + offset
	}
	def descriptionText = temperatureScale == 'C' ? '{{ device.displayName }} was {{ value }}°C':
			'{{ device.displayName }} was {{ value }}°F'

	return [
		name: 'temperature',
		value: value,
		descriptionText: descriptionText,
		translatable: true,
		unit: temperatureScale
	]
}

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

private Map getLuminanceResult(rawValue) {
	log.debug "Luminance rawValue = ${rawValue}"

	if (luxOffset) {
		def offset = luxOffset as int
		def v = rawValue as int
		rawValue = v + offset
	}
    
	def result = [
		name: 'illuminance',
		value: '--',
		translatable: true,
 		unit: 'lux'
	]
    
    result.value = rawValue as Integer
    return result
}

/*
	getBatteryResult
*/
//TODO: needs calibration
private Map getBatteryResult(rawValue) {
	//log.debug "Battery rawValue = ${rawValue}"

	def result = [
		name: 'battery',
		value: '--',
		translatable: true
	]

	def volts = rawValue / 10

	if (rawValue == 0 || rawValue == 255) {}
	else {
		if (volts > 3.5) {
			result.descriptionText = "{{ device.displayName }} battery has too much power: (> 3.5) volts."
		}
		else {
			if (device.getDataValue("manufacturer") == "SmartThings") {
				volts = rawValue // For the batteryMap to work the key needs to be an int
				def batteryMap = [28:100, 27:100, 26:100, 25:90, 24:90, 23:70,
								  22:70, 21:50, 20:50, 19:30, 18:30, 17:15, 16:1, 15:0]
				def minVolts = 15
				def maxVolts = 28

				if (volts < minVolts)
					volts = minVolts
				else if (volts > maxVolts)
					volts = maxVolts
				def pct = batteryMap[volts]
				if (pct != null) {
					result.value = pct
					result.descriptionText = "{{ device.displayName }} battery was {{ value }}%"
				}
			}
			else {
				def minVolts = 2.1
				def maxVolts = 3.0
				def pct = (volts - minVolts) / (maxVolts - minVolts)
				def roundedPct = Math.round(pct * 100)
				if (roundedPct <= 0)
					roundedPct = 1
				result.value = Math.min(100, roundedPct)
				result.descriptionText = "{{ device.displayName }} battery was {{ value }}%"
			}
		}
	}

	return result
}
/*
	parseCustomMessage
*/
private Map parseCustomMessage(String description) {
	Map resultMap = [:]
	if (description?.startsWith('temperature: ')) {
		def value = zigbee.parseHATemperatureValue(description, "temperature: ", getTemperatureScale())
		resultMap = getTemperatureResult(value)
	}
    
    if (description?.startsWith('illuminance: ')) {
    log.warn "value: " + description.split(": ")[1]
            log.warn "proc: " + value

		def value = zigbee.lux( description.split(": ")[1] as Integer ) //zigbee.parseHAIlluminanceValue(description, "illuminance: ", getTemperatureScale())
		resultMap = getLuminanceResult(value)
	}
	return resultMap
}

/*
	parseReportAttributeMessage
*/
private List parseReportAttributeMessage(String description) {
	Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param ->
		def nameAndValue = param.split(":")
		map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
	}

	List result = []
    
    // Temperature
	if (descMap.cluster == "0402" && descMap.attrId == "0000") {
		def value = getTemperature(descMap.value)
		result << getTemperatureResult(value)
	}
    
    // Motion
   	else if (descMap.cluster == "0406" && descMap.attrId == "0000") {
    	result << getMotionResult(descMap.value)
	}
    
    // Battery
	else if (descMap.cluster == "0001" && descMap.attrId == "0020") {
		result << getBatteryResult(Integer.parseInt(descMap.value, 16))
	}
    
    // Luminance
    else if (descMap.cluster == "0402" ) { //&& descMap.attrId == "0020") {
		log.error "Luminance Response " + description
        //result << getBatteryResult(Integer.parseInt(descMap.value, 16))
	}

	return result
}


/*
	parseCatchAllMessage
*/
private Map parseCatchAllMessage(String description) {
	Map resultMap = [:]
	def cluster = zigbee.parse(description)
//	log.debug cluster
	if (shouldProcessMessage(cluster)) {
		switch(cluster.clusterId) {
			case 0x0001:
				// 0x07 - configure reporting
				if (cluster.command != 0x07) {
					resultMap = getBatteryResult(cluster.data.last())
				}
			break

			case 0x0400:
            	if (cluster.command == 0x07) { // Ignore Configure Reporting Response
                	if(cluster.data[0] == 0x00) {
						log.trace "Luminance Reporting Configured"
						sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
					}
					else {
						log.warn "Luminance REPORTING CONFIG FAILED- error code:${cluster.data[0]}"
					}
				}
				else {
            		log.debug "catchall : luminance" + cluster
                	resultMap = getLuminanceResult(cluster.data.last());
                }

			break
            
			
            
			case 0x0402:
				if (cluster.command == 0x07) {
					if(cluster.data[0] == 0x00) {
						log.trace "Temperature Reporting Configured"
						sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
					}
					else {
						log.warn "TEMP REPORTING CONFIG FAILED- error code:${cluster.data[0]}"
					}
				}
				else {
					// temp is last 2 data values. reverse to swap endian
					String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
					def value = getTemperature(temp)
					resultMap = getTemperatureResult(value)
				}
			break
		}
	}

	return resultMap
}

private boolean shouldProcessMessage(cluster) {
	// 0x0B is default response indicating message got through
	boolean ignoredMessage = cluster.profileId != 0x0104 ||
	cluster.command == 0x0B ||
	(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
	return !ignoredMessage
}


// This seems to be IAS Specific and not needed we are not really a motion sensor
def enrollResponse() {
//	log.debug "Sending enroll response"
//	String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
//	[
//		//Resending the CIE in case the enroll request is sent before CIE is written
//		"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
//		"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
//		//Enroll Response
//		"raw 0x500 {01 23 00 00 00}", "delay 200",
//		"send 0x${device.deviceNetworkId} 1 1", "delay 200"
//	]
}

def configureHealthCheck() {
    Integer hcIntervalMinutes = 12
    refresh()
    sendEvent(name: "checkInterval", value: hcIntervalMinutes * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
}

def updated() {
    log.debug "in updated()"
    configureHealthCheck()
}

def ping() {
    return zigbee.onOffRefresh()
}





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
}

Sweet. I will give it a try when I get home and let you know.

Any chance you have figured what what the ‘Configure’ button is for?

The configure button is not needed. It was simply for testing to rerun the configuration routine in the device handler. I forgot to remove it.

I have been immensely busy with work tasks over the last month and I am also not awaiting a replacement hub from smart things as it seems mine has kicked the bucket. Once I get it I will take another look at this thread and try and incorporate the feedback.

PS - ST its been a week since you said I would get a tracking number and still nothing :frowning:

@Stephen_McLaughlin it’s all cool. You laid down the foundation for this which we are all immensely great full for. The community is helping out which is great to see. Personally I would love to be able to give back more on the coding side of things but just don’t have the understanding of it all… yet :stuck_out_tongue_closed_eyes:

ST - come on guys - we expect better than that from you. Give the man his replacement! :wink:

So great that this handler exists. Looking forward to the flexibility it will give to my setup.

Is it possible to use the Hue Sensors via this device handler and still via the hue bridge? If so, does anyone know why the two sensors I’ve added to ST are showing up as unreachable in the official hue app?

@vster it is showing unreachable as you have disconnected at the sensor, not at the hub. It will stay as a device until you delete it. It is best you do delete from hue app as otherwise it will keep trying to poll for something not there and will just add an overhead to your system

1 Like

I’m starting to think that this sensor isn’t very usable for triggering based off the lux value. It seems that the lux measurement is tied to the motion status; once the motion reports as active, the lux reading drops to 0.

This causes a serious problem when a CoRE routine is checking for both motion active and a certain lux value. A short time after the motion goes active, the lux drops to 0 and triggers the piston, even if the lux value was above the threshold when the motion was first sensed.

If this could be fixed in the DTH that would be great, but that is beyond my “seek and poke” approach to groovy programming at the moment. :slight_smile:

In the meantime, I guess I need to rethink my CoRE piston and maybe test for luminance first, and then lock it out somehow.