Wired doorbell to HE?

I use the Nexia Doorbell device. Works perfectly, but chews through AAA batteries a bit. If you have a traditional doorbell with a chime, all you have to do is wire the Nexia sensor inside the chime itself. Here's mine:

I think the Sage one might be better since it runs straight off the AC voltage supplied by the transformer. Plus it’s 1/10th the price and Zigbee (which I prefer). I will test it when I get it this weekend. Worse case I’m out $5.00

Heck, I bet those'd pay for themselves in battery savings in no time.

Also, I think Zigbee is winning the smarthome race.

1 Like

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