Leaksmart Water Sensor

…So which post has working code? I seem to get any of them to work.

EDIT: I got it now… Took a while for it to start working. A while = 10-15 seconds.

Hey everyone. First thank you to everyone who got this working. I took the code above, cleaned it up made it so you don’t have to use the simulator to get it working.

Note: This code is by no means 100% all my work. This device handler is the work of SmartThings, @dhelm2, & @John_Luikart. I’ve simply adjusted for format, added better logging, and updated the code to not require the simulator to configure. The device configures itself sort of. I’ll continue to refine this this week.

1 - Pair
2 - Switch device handler
3 - Navigate to the device on your app, press the configure cog, hit done.
4 - Hit refresh.

Not sure if step 4 is required, for each of my 6 devices it took anywhere between 5 seconds and 2 minutes after #3 to begin reporting data. Hitting refresh seems to make that happen faster.

2 Likes

Edit: It worked after I ran it in the simulator. Had to run simulator for all 3 devices for them to start working.

I tried those exact steps multiple times. Tried a second water sensor. Tried unpair and paring again. Deleted and added the code again. I still see the same thing in the logs about 0b02 not supported when I refresh (after changing log level to debug). Nothing updates when I set off the alarm. Even if I refresh during the alarm. What am I missing?

leakSmart – parse(read attr - raw: CEF5010B0206000086, dni: CEF5, endpoint: 01, cluster: 0B02, size: 06, attrId: 0000, result: unsupported attr.

You hit the refresh button and not the “pull down” refresh right?

Attempting to resolve the pairing device identification and unsupported attr error. I have not tested this. Battery and temp work, not sure if alarm does. Give it a try if you want, otherwise I’ll test tonight.

1 Like

Hi All,

My name is Augustin, I’m new to SmartThings and very excited…:slight_smile: I have one leaksmart valve working, thanks to krlaframboise https://community.smartthings.com/t/release-leaksmart-water-valve/48669?u
I am trying to use the leaksmart sensors, I think Eric is doing a great job. The pairing is working just fine but the alarm no.

@Augustin - When you paired did smartthings recognize it was a leakSmart sensor or did you have to pick it? I am not at home so I can’t test my latest changes. I’ll revert the change I made that I think might have broken the alarm. The updated version can be found here…

1 Like

Yes, was recognized automatically today at noon.
If I hit refresh button, has like 30 second delay or more and is showing just the temperature. The temp I think is off … +4 degrees.
I will try the update later. Thanks @whoismoses

Update: Updated version, paired and recognized, no battery, no temperature, no alert but now I can see it on the dashboard. Maybe I am doing something wrong… do I need to restart the Hub?

Just got home, taking a look now but all of mine are reporting both battery & temp. No alarm. Fixing now.

Maybe this helps … I just removed the smartapps links and now I can see battery and temp. No alarm.

I have it working again, however you have to use the simulator to configure it. Still debugging.

1 Like

Here’s what I have updated for my device handler. I don’t have a new device to add to test with, but I’ve included the configure as part of the refresh, instead of the reverse as before.

/*

  • Copyright 2016 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: “leakSmart Moisture Sensor”,namespace: “smartthings”, author: “SmartThings”, category: “C2”) {
capability "Configuration"
capability "Battery"
capability "Refresh"
capability "Temperature Measurement"
capability “Water Sensor”

    fingerprint profileId: "0104", inClusters: "0000,0001,0003,0020,0402,0B02,FC02", outClusters: "0003,0019", manufacturer: "WAXMAN", model: "leakSMART Water Sensor V2", deviceJoinName: "leakSmart Water Sensor"
}

simulator {

}

preferences {
    section {
        image(name: 'educationalcontent', multiple: true, images: [
			"http://cdn.device-gse.smartthings.com/Moisture/Moisture1.png",
			"http://cdn.device-gse.smartthings.com/Moisture/Moisture2.png",
			"http://cdn.device-gse.smartthings.com/Moisture/Moisture3.png"
        ])
    }
    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
    }
}

tiles(scale: 2) {
    multiAttributeTile(name:"water", type: "generic", width: 6, height: 4){
        tileAttribute ("device.water", key: "PRIMARY_CONTROL") {
            attributeState "dry", label: "Dry", icon:"st.alarm.water.dry", backgroundColor:"#ffffff"
            attributeState "wet", label: "Wet", icon:"st.alarm.water.wet", backgroundColor:"#53a7c0"
        }
    }
    valueTile("temperature", "device.temperature", inactiveLabel: false, 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:""
    }
    standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
        state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
    }

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

}

def parse(String description) {
log.debug “description: $description”

Map map = [:]

if (description?.startsWith(‘catchall:’)) {
map = parseCatchAllMessage(description)
}
else if (description?.startsWith(‘read attr -’)) {
map = parseReportAttributeMessage(description)
}
else if (description?.startsWith('temperature: ')) {
map = parseCustomMessage(description)
}

    log.debug "Parse returned $map"

def result = map ? createEvent(map) : null

if (description?.startsWith(‘enroll request’)) {
List cmds = enrollResponse()
log.debug "enroll response: ${cmds}"
result = cmds?.collect { new physicalgraph.device.HubAction(it) }
}
return result
}

private Map parseCatchAllMessage(String description) {
Map resultMap = [:]
def cluster = zigbee.parse(description)
if (shouldProcessMessage(cluster)) {
switch(cluster.clusterId) {
case 0x0001:
log.debug "001 Cluster Data: ${cluster.data}"
resultMap = getBatteryResult(cluster.data.last())
break

    case 0x0402:
       // temp is last 2 data values. reverse to swap endian
    	log.debug "402 Cluster Data: ${cluster.data}"
        String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
       def value = getTemperature(temp)
        resultMap = getTemperatureResult(value)
        break
    case 0x0B02:
    	log.debug "B02 Cluster Data: ${cluster.data}"
        String temp = cluster.data[2];
        log.debug "B02 temp data ${temp}"
        return parseAlarmCode(temp)
		break
   }
}

else {
log.debug “Did not process message ${cluster}”
}
return resultMap
}

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

private Map parseReportAttributeMessage(String description) {
Map descMap = (description - “read attr - “).split(”,”).inject([:]) { map, param ->
def nameAndValue = param.split(":")
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
}
log.debug “Desc Map: $descMap”

Map resultMap = [:]
if (descMap.cluster == “0402” && descMap.attrId == “0000”) {
def value = getTemperature(descMap.value)
resultMap = getTemperatureResult(value)
}
else if (descMap.cluster == “0001” && descMap.attrId == “0020”) {
resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16))
}
else if (descMap.cluster == “0b02” && descMap.attrId == “0000”) {
log.debug “Parsing cluster B02 data”
//resultMap =
}

return resultMap
}

private Map parseCustomMessage(String description) {
Map resultMap = [:]
if (description?.startsWith('temperature: ')) {
def value = zigbee.parseHATemperatureValue(description, "temperature: ", getTemperatureScale())
resultMap = getTemperatureResult(value)
}
return resultMap
}

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

private Map getBatteryResult(rawValue) {
log.debug "Battery rawValue = ${rawValue}"
def linkText = getLinkText(device)

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

]

def volts = rawValue / 10

if (rawValue == 0 || rawValue == 255) {}
else {
    if (volts > 4.5) {
        result.value = 100
        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 = 4.5
            def pct = (volts - minVolts) / (maxVolts - minVolts)
            result.value = Math.min(100, (int) pct * 100)
            result.descriptionText = "{{ device.displayName }} battery was {{ value }}%"
        }
    }
}

return result

}

private Map getTemperatureResult(value) {
log.debug 'TEMP’
if (tempOffset) {
def offset = tempOffset as int
def v = value as int
value = v + offset
}
def descriptionText
if ( temperatureScale == ‘C’ )
descriptionText = '{{ device.displayName }} was {{ value }}°C’
else
descriptionText = ‘{{ device.displayName }} was {{ value }}°F’

return [
    name: 'temperature',
value: value,
descriptionText: descriptionText,
translatable: true

]
}

private Map getMoistureResult(value) {
log.debug "water"
def descriptionText
if ( value == “wet” )
descriptionText = '{{ device.displayName }} is wet’
else
descriptionText = '{{ device.displayName }} is dry’
return [
name: ‘water’,
value: value,
descriptionText: descriptionText,
translatable: true
]
}

private Map parseAlarmCode(value) {
log.debug “parse alarm code ${value}”

Map resultMap = [:]

switch(value) {
    case "1": // Closed/No Motion/Dry
    	log.debug "dry"
        resultMap = getMoistureResult('dry')
        break

    case "17": // Open/Motion/Wet
    	log.debug "wet"
        resultMap = getMoistureResult('wet')
        break
        }

return resultMap

}

def refresh() {
log.debug "Refreshing"
def refreshCmds = [
zigbee.readAttribute(0x0402, 0x0000), “delay 200”,
zigbee.readAttribute(0x0001, 0x0020), “delay 200”

    //for mfg and model
    //"str rattr 0x${zigbee.deviceNetworkId} 0x${zigbee.endpointId} 0 4", "delay 200",
	//"str rattr 0x${zigbee.deviceNetworkId} 0x${zigbee.endpointId} 0 5"
   
]

return refreshCmds + configure() //send config as part of the refresh

}

def configure() {
def configCmds = [

	zigbee.configureReporting(0x0001, 0x0020, 0x20, 30, 21600, 0x01), "delay 500",
  	zigbee.configureReporting(0x0402, 0x0000, 0x29, 30, 3600, 0x0064), "delay 500",
    zigbee.configureReporting(0x0b02, 0x0000, 0x10, 0, 3600, null), "delay 500"

]
log.debug "Sending config commands"
return configCmds
}

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

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

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
}

1 Like

This works for me now. Tested it on 6 sensors. Pain in the ass, but it works.

Post Installation Configuration

  1. Navigate to the device on your SmartThings app.
  2. Press the gear icons.
  3. Press done (or update the log level and done).
  4. Wait about 10 - 15 seconds.
  5. Press the “refresh” button.
  6. Wait about 10 - 15 seconds.
  7. Press the “configure” button.
  8. Wait about 10 - 15 seconds.
  9. Temperature and battery level should begin to appear.
  10. If they don’t begin to appear, do another cycle of 2-8.

Just tested with one sensor that i got and it worked as described! Great work!

I can’t figure out why the configure command doesn’t finish during the regular setup process which is why you need the post installation steps. I’m glad it worked.

Found that removing one battery from the sensor for a few seconds and then putting it back in works much better than steps 2-8 :slight_smile:

THANK YOU for the DTH.

Thanks. I cant figure out what is going on during the standard install / update process. During both procedures the updated()/installed() method gets called with then calls initialize() which then call configure(). Configure seems to start but never finish. I’ve try surrounding everything with try / catch’s but no dice.

I tried your code but it has that same Java.Lang exception error, so I reverted back to dhelm2’s code I posted since it works despite the need for setup via the simulator. Looking forward to working code that doesn’t need extra configuration steps outside the app.

I’m still having trouble getting the Dry/Wet status to update. Below is the log data that I think is showing the change in status as it correlates with me shorting the sensing pins at 5:22:14 and then removing the short at 5:22:34. Any idea why I’m seeing different results?

FYI, pairing and temperature/battery refreshing all went smoothly.

58e27323-c02a-4845-b3a7-42517d5290d2 5:22:34 PM: debug leakSmart – map = [:].
58e27323-c02a-4845-b3a7-42517d5290d2 5:22:34 PM: debug leakSmart – Desc Map: [raw:9E0E010B020801810100, dni:9E0E, endpoint:01, cluster:0B02, size:08, attrId:8101, encoding:01, value:00].
58e27323-c02a-4845-b3a7-42517d5290d2 5:22:34 PM: debug leakSmart – parse(read attr - raw: 9E0E010B020801810100, dni: 9E0E, endpoint: 01, cluster: 0B02, size: 08, attrId: 8101, encoding: 01, value: 00.
58e27323-c02a-4845-b3a7-42517d5290d2 5:22:14 PM: debug leakSmart – map = [:].
58e27323-c02a-4845-b3a7-42517d5290d2 5:22:14 PM: debug leakSmart – Desc Map: [raw:9E0E010B020801811100, dni:9E0E, endpoint:01, cluster:0B02, size:08, attrId:8101, encoding:11, value:00].
58e27323-c02a-4845-b3a7-42517d5290d2 5:22:14 PM: debug leakSmart – parse(read attr - raw: 9E0E010B020801811100, dni: 9E0E, endpoint: 01, cluster: 0B02, size: 08, attrId: 8101, encoding: 11, value: 00.

I’m also including the log section from when I asked the device to configure in case that reveals anything:

Configuration Log

58e27323-c02a-4845-b3a7-42517d5290d2 5:21:40 PM: debug leakSmart – map = [:].
58e27323-c02a-4845-b3a7-42517d5290d2 5:21:40 PM: debug leakSmart – Desc Map: [raw:9E0E010B0206000086, dni:9E0E, endpoint:01, cluster:0B02, size:06, attrId:0000, result:unsupported attr].
58e27323-c02a-4845-b3a7-42517d5290d2 5:21:40 PM: debug leakSmart – parse(read attr - raw: 9E0E010B0206000086, dni: 9E0E, endpoint: 01, cluster: 0B02, size: 06, attrId: 0000, result: unsupported attr.
58e27323-c02a-4845-b3a7-42517d5290d2 5:21:40 PM: debug leakSmart – map = [name:battery, value:91, translatable:true, descriptionText:{{ device.displayName }} battery was {{ value }}%].
58e27323-c02a-4845-b3a7-42517d5290d2 5:21:40 PM: debug leakSmart – Battery rawValue = 43.
58e27323-c02a-4845-b3a7-42517d5290d2 5:21:40 PM: debug leakSmart – Desc Map: [raw:9E0E0100010A200000202B, dni:9E0E, endpoint:01, cluster:0001, size:0A, attrId:0020, result:success, encoding:20, value:2b].
58e27323-c02a-4845-b3a7-42517d5290d2 5:21:39 PM: debug leakSmart – parse(read attr - raw: 9E0E0100010A200000202B, dni: 9E0E, endpoint: 01, cluster: 0001, size: 0A, attrId: 0020, result: success, encoding: 20, value: 2b.
58e27323-c02a-4845-b3a7-42517d5290d2 5:21:39 PM: debug leakSmart – map = [name:temperature, value:82, descriptionText:{{ device.displayName }} was {{ value }}°F, translatable:true].
58e27323-c02a-4845-b3a7-42517d5290d2 5:21:39 PM: debug leakSmart – Begin getTemperatureResult(82).
58e27323-c02a-4845-b3a7-42517d5290d2 5:21:39 PM: debug leakSmart – 402 Cluster Data: [0, 0, 0, 41, 17, 11].
58e27323-c02a-4845-b3a7-42517d5290d2 5:21:39 PM: debug leakSmart – parse(catchall: 0104 0402 01 01 0140 00 9E0E 00 00 0000 01 01 00000029110B.
58e27323-c02a-4845-b3a7-42517d5290d2 5:21:38 PM: debug leakSmart – map = [:].
58e27323-c02a-4845-b3a7-42517d5290d2 5:21:38 PM: debug leakSmart – Did not process message SmartShield(clusterId: 0x0b02, command: 0x07, data: [0x86, 0x00, 0x00, 0x00], destinationEndpoint: 0x01, direction: 0x01, isClusterSpecific: false, isManufacturerSpecific: false, manufacturerId: 0x0000, messageType: 0x00, number: null, options: 0x0140, profileId: 0x0104, senderShortId: 0x9e0e, sourceEndpoint: 0x01, text: null).
58e27323-c02a-4845-b3a7-42517d5290d2 5:21:38 PM: debug leakSmart – parse(catchall: 0104 0B02 01 01 0140 00 9E0E 00 00 0000 07 01 86000000.
58e27323-c02a-4845-b3a7-42517d5290d2 5:21:37 PM: debug leakSmart – map = [:].
58e27323-c02a-4845-b3a7-42517d5290d2 5:21:37 PM: debug leakSmart – Did not process message SmartShield(clusterId: 0x0402, command: 0x07, data: [0x00], destinationEndpoint: 0x01, direction: 0x01, isClusterSpecific: false, isManufacturerSpecific: false, manufacturerId: 0x0000, messageType: 0x00, number: null, options: 0x0140, profileId: 0x0104, senderShortId: 0x9e0e, sourceEndpoint: 0x01, text: null).
58e27323-c02a-4845-b3a7-42517d5290d2 5:21:37 PM: debug leakSmart – parse(catchall: 0104 0402 01 01 0140 00 9E0E 00 00 0000 07 01 00.
58e27323-c02a-4845-b3a7-42517d5290d2 5:21:36 PM: debug leakSmart – map = [:].
58e27323-c02a-4845-b3a7-42517d5290d2 5:21:36 PM: debug leakSmart – Did not process message SmartShield(clusterId: 0x0001, command: 0x07, data: [0x00], destinationEndpoint: 0x01, direction: 0x01, isClusterSpecific: false, isManufacturerSpecific: false, manufacturerId: 0x0000, messageType: 0x00, number: null, options: 0x0140, profileId: 0x0104, senderShortId: 0x9e0e, sourceEndpoint: 0x01, text: null).
58e27323-c02a-4845-b3a7-42517d5290d2 5:21:36 PM: debug leakSmart – parse(catchall: 0104 0001 01 01 0140 00 9E0E 00 00 0000 07 01 00.
58e27323-c02a-4845-b3a7-42517d5290d2 5:21:34 PM: debug Configuring Reporting, IAS CIE, and Bindings.

Which error, can you post the logs? Off what event? Refresh or wet?