Wired doorbell to HE?

I thought the Sage one runs off of a battery? Mine should here on Saturday.

It's running on CR2 3V battery.

1 Like

Oh my bad. I just assumed.

Pretty sweet - I bought the sage sensor on eBay for $5 and now have my doorbell integrated into hubitat!

Next step - I’d love to have hubitat pull a still photo from my unifi camera and text it to me. I don’t think this is possible at this time, unless someone knows something I don’t.

1 Like

Anyone developed a driver for this SAGE door bell sensor yet?

I found one here

I made some changes for my needs.

Update: commented out SendEvents per @ogiewon recommendation below

/**
 *  SAGE Doorbell Sensor
 *
 *  For device information and images, questions or to provide feedback on this device handler, 
 *  please visit: 
 *
 *      darwinsden.com/sage-doorbell
 *
 *  White wire: common
 *  Green wire: doorbell 1 / front
 *  Yellow wire: doorbell 2 / back
 *
 * Factory reset:
 * Remove the plastic cover and the battery.
 * Press and hold the tiny RESET button (next to where the wires attach to the circuit board) while you reinstall the battery.
 * Continue holding RESET until the red LED blinks.
 *
 *  Copyright 2016 DarwinsDen.com
 *
 *  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.
 *
 *	Author: Darwin@DarwinsDen.com
 *	Date: 2016-06-13
 *
 *	Changelog:
 *
 *  0.40 (03/23/2017) - set numberOfButtons attribute for those smart apps that rely on this
 *  0.30 (11/20/2016) - Removed non-operational battery capability; was preventing device display on 2.2.2 mobile app
 *  0.20 (08/02/2016) - Added preference option for allowed time between presses to eliminate duplicate notifications on some systems
 *  0.10 (06/13/2016) - Initial 0.1 pre-beta Test Code
 *
 */
 
metadata {
	definition (name: "SAGE Doorbell Sensor", namespace: "darwinsden", author: "darwin@darwinsden.com") {
    	//capability "Battery"
		capability "Configuration"
        capability "Pushable Button"
		capability "Refresh"
     
        command "enrollResponse"
 	   
        fingerprint endpointId: "12", inClusters: "0000,0003,0009,0001", outClusters: "0003,0006,0008,0019", model: "Bell", manufacturer: "Echostar"
    }
      
    preferences {
        input "timeBetweenPresses", "number", title: "Seconds allowed between presses (increase this value to eliminate duplicate notifications)",  defaultValue: 10,  displayDuringSetup: true, required: false	
    } 
}
 
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)
	}
    
	//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 hubitat.device.HubAction(it) }
    }
    return result
}
 
private Map parseCatchAllMessage(String description) {
    Map resultMap = [:]
    def cluster = zigbee.parse(description)
    if (shouldProcessMessage(cluster)) { 
        //log.debug ("cluster: $cluster")
        switch(cluster.clusterId) {
            case 0x0006:
            	resultMap = getDoorbellPressResult(cluster)
                break
        }
    }
    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 == "0001" && descMap.attrId == "0020") {
        log.debug descMap.value
		resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16))
	}
 
	return resultMap
}

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 > 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)
				result.value = Math.min(100, (int) pct * 100)
				result.descriptionText = "{{ device.displayName }} battery was {{ value }}%"
			}
		}
	}

	return result
}

private Map getDoorbellPressResult(cluster) {
    def linkText = getLinkText(device)
    def buttonNumber = (cluster.command as int)
    def result = [:]
    
    // map buttons per Hughes described defaults for green and yellow wires
    
    switch(buttonNumber) {
        case 0: 
            if (!isDuplicateCall(state.lastButton2Updated, state.timeBetweenPresses) )
            {
		       log.debug ("Rear Doorbell Pressed!")       
               result = [ name: 'pushed', value: "2", isStateChange: true ]
               //sendEvent(name: "pushed", value: "2", isStateChange: true)
            }
            state.lastButton2Updated = new Date().time	
            break

        case 1: 
            if (!isDuplicateCall(state.lastButton1Updated, state.timeBetweenPresses) )
            {		
		       log.debug ("Front Doorbell Pressed!")
               result = [ name: 'pushed', value: "1", isStateChange: true ]
               //sendEvent (name: "pushed", value: "1", isStateChange: true) 
            }
            state.lastButton1Updated = new Date().time
    }
    return result
}

def refresh() {
	log.debug "Refreshing Battery"
    
    def refreshCmds = [
        "he rattr 0x${device.deviceNetworkId} 18 0x0001 0x20", "delay 500", 
	]

    sendEvent(name: "numberOfButtons", value: 2, displayed: false)
    
    setPrefs()
    
	return refreshCmds + enrollResponse()
}

def configure() {

    setPrefs()
	String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
	log.debug "Configuring Reporting, IAS CIE, and Bindings."
	def configCmds = [
		"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
		"send 0x${device.deviceNetworkId} 1 1", "delay 500",

		"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 500",
		"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}",		//checkin time 6 hrs
		"send 0x${device.deviceNetworkId} 1 1", "delay 500",

		"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 500",
		"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
		"send 0x${device.deviceNetworkId} 1 1", "delay 500"
	]
    return configCmds + refresh() // send refresh cmds as part of config
}

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}",
		"send 0x${device.deviceNetworkId} 1 1", "delay 200"
	]
}

private isDuplicateCall(lastRun, allowedEverySeconds) {
	def result = false
	if (lastRun) {
		result =((new Date().time) - lastRun) < (allowedEverySeconds * 1000)
	}
	result
}

def setPrefs() 
{
   log.debug ("setting preferences")
   if (timeBetweenPresses == null)
   {
      state.timeBetweenPresses = 10
   }
      else if (timeBetweenPresses < 0)
   {
      state.timeBetweenPresses = 0
   }
   else
   {
      state.timeBetweenPresses = timeBetweenPresses
   }  
}

def updated()
{
   setPrefs()
}

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

I just installed the Sage sensor after it came in the mail today. Seems to work well and it’s very fast as you’d expect from a Zigbee sensor. Tomorrow I’ll try the driver y’all posted here. Thanks!

Care to elaborate on what your tweaks are? I just received one of these Sage Doorbell devices today as well. Thanks!

Just fixed an error and redo some logging stuffs.

1 Like

Thanks. I’ll give your version a try this weekend.

@cuboy29 have you noticed the driver produces duplicate pushed events? I figured out a simple fix. Just comment out the “sendEvent()” calls as shown below.

private Map getDoorbellPressResult(cluster) {
    def linkText = getLinkText(device)
    def buttonNumber = (cluster.command as int)
    def result = [:]
    
    // map buttons per Hughes described defaults for green and yellow wires
    
    switch(buttonNumber) {
        case 0: 
            if (!isDuplicateCall(state.lastButton2Updated, state.timeBetweenPresses) )
            {
		       log.debug ("Rear Doorbell Pressed!")       
               result = [ name: 'pushed', value: "2", isStateChange: true ]
               //sendEvent(name: "pushed", value: "2", isStateChange: true)
            }
            state.lastButton2Updated = new Date().time	
            break

        case 1: 
            if (!isDuplicateCall(state.lastButton1Updated, state.timeBetweenPresses) )
            {		
		       log.debug ("Front Doorbell Pressed!")
               result = [ name: 'pushed', value: "1", isStateChange: true ]
               //sendEvent (name: "pushed", value: "1", isStateChange: true) 
            }
            state.lastButton1Updated = new Date().time
    }
    return result
}
3 Likes

The driver worked fine for me. I actually modified it for my purposes and made it a Contact Switch as opposed to Pushable Button. With it being a Contact Switch I now get notifications in HomeKit when the doorbell rings. Exactly what I needed. Thanks a lot for the driver.

Here's the modified code in case anybody's interested. I removed the "back door" doorbell but it's easy to add in. I also removed all other functionality (except the actual sensor opening/closing). The code below is all you need for the it to work as a sensor.

metadata {
    definition (name: "SAGE Doorbell Sensor", namespace: "darwinsden", author: "darwin@darwinsden.com") {
        capability "Contact Sensor"
		capability "Sensor"
        capability "Configuration"
        fingerprint endpointId: "12", inClusters: "0000,0003,0009,0001", outClusters: "0003,0006,0008,0019", model: "Bell", manufacturer: "Echostar"
    }
}

def ensureClosed(){
    sendEvent(name: "contact", value: "closed", displayed: false, isStateChange: true) 
}
 
def parse(String description) {
    def cluster = zigbee.parse(description)
    if(cluster.profileId != 0x0104 || cluster.command == 0x0B || cluster.command == 0x07 || (cluster.data.size() > 0 && cluster.data.first() == 0x3e)){
        return
    }
    if(cluster.clusterId == 0x0006){
        //log.debug("ding dong")
        sendEvent(name: "contact", value: "open", displayed: false, isStateChange: true)
        runIn(1, ensureClosed)
    }
}

def refresh() {
       configure()
 }

def configure() {
   ensureClosed()
}

def updated()
{
    configure()
}

Dan

I've already notified the poster of the driver.

The driver on github has different handling for button 1 and button 2. I wanted to get your advice on the correct code.

Below is the code as posted on github, the "result =" is different for button 1 and 2. For me button 1 (front) worked properly button 2 (rear) produced duplicate events, leading to your fix above.

However when I commented out the "sendEvent" line of button 1, no events occurred for button 1 and single events for button 2.

My question: Which "result" line would be the preferred code?

Mike

switch(buttonNumber) {
    case 0: 
        if (!isDuplicateCall(state.lastButton2Updated, state.timeBetweenPresses) )
        {
	       //log.debug ("BUTTON2 PRESS!")               
           result = [ name: 'pushed', value: "2", isStateChange: true ]
           sendEvent(name: "pushed", value: "2", isStateChange: true)
        }
        state.lastButton2Updated = new Date().time	
        break

    case 1: 
        if (!isDuplicateCall(state.lastButton1Updated, state.timeBetweenPresses) )
        {		
	       //log.debug ("BUTTON1 PRESS!")
           result = [ name: 'button', value: "pushed", data: [buttonNumber: 1], isStateChange: true]
           sendEvent (name: "pushed", value: "1", isStateChange: true) 
        }
        state.lastButton1Updated = new Date().time
}
return result

}

This is mine...

switch(buttonNumber) {
181
        case 0: 
182
            if (!isDuplicateCall(state.lastButton2Updated, state.timeBetweenPresses) )
183
            {
184
               log.debug ("Rear Doorbell Pressed!")       
185
               result = [ name: 'pushed', value: "2", isStateChange: true ]
186
               //sendEvent(name: "pushed", value: "2", isStateChange: true)
187
            }
188
            state.lastButton2Updated = new Date().time  
189
            break
190
​
191
        case 1: 
192
            if (!isDuplicateCall(state.lastButton1Updated, state.timeBetweenPresses) )
193
            {       
194
               log.debug ("Front Doorbell Pressed!")
195
               result = [ name: 'pushed', value: "1", isStateChange: true ]
196
               //sendEvent (name: "pushed", value: "1", isStateChange: true) 
197
            }
198
            state.lastButton1Updated = new Date().time
199
    }
200
    return result
2 Likes

Thanks

Mike

here is my version of the driver. I've taken what others have done before, then added parameters to store custom names for each contact. Useful for logs.

4 Likes

Hello Trunzoc.
I had noticed many postings with code for this device. Mine was delivered yesterday. Can you tell me if people are free to use your code please? And if so..is it as simple as copy and paste to the drivers code? Anything else needed?
Thank You
Mac

Take a look at the code itself for the license info displayed pretty prominently towards the top.

As a zigbee device all you should have to do is add the driver code to your hub. I don’t have one of these, so I don’t know for sure the hub auto-assigns the driver correctly, but if it doesn’t you can manually reassign the driver in the device’s details page after it has paired, then save and hit the configure button at the top of the page.

Yep. Free to use, as all user drivers and apps should be!

1 Like