Saturday, 4 November 2023

Announce ReoLink Doorbell on Echo Devices

I recently installed a Reolink POE Video Doorbell, and like it a lot. Its got great picture, doesn't require web connectivity, and integrates really well with my Unraid.html">Blue Iris set up
. The only issue I have is that notifications on my mobile are poor and often missed. The bundled chime works OK inside the house, but when I'm in my detached office, I can often miss visitors.


I thought it would be nice to have the doorbell ping my office Alexa when pressed. Of course, there's a skill for that, but as someone with a penchant for Node-Red automations and too much time on my hands, a project was born....

And this is what I came up with. A Node-RED flow that allows me set one or more Echo devices to announce doorbell presses;


Sure, the native skill does something similar, with extra bells and whistles, but I like this because;
  • The included Echo devices can be specified on or off by voice
  • I can easily configure the notification sound and string
  • I can name the command whatever I like
  • It works on devices with or without screens
  • I made it, and learned a lot in doing so
  • Likely works without internet  (confirmed not working as Alexa won't even process wake word)

I was inspired to undertake this project having seen this solution by u/Loafdude in this Reddit post where notifications are sent to Google Home / Nest. I adapted that for Alexa, and added the ability to configure individual Echo devices to be included in the broadcast.

Here's the flow. The code is included at the bottom of the page.



This works chiefly by utilising the amazon-echo-device node from node-red-contrib-local-alexa-devices. This allows for a device odes to be set up in Node-RED that can be discovered and therefore addressed by Alexa.

These nodes manifest as lights in  the Alexa app, and can therefore accept on and off commands, as well as dim requests and a few others. The neat thing is that these works like regular smarthome devices, and I can use nodes from node-red-contrib-alexa-remote2-applestrudel to interact with it from within Node-RED.

There are two main parts to this flow;


MANAGE REOLINK EVENTS

This uses part of the original google-focused solution to listen for OnVif events, filter out 'Visitor' messages that are generated from a bell press and then push voice notifications to the user selected Echo devices.



When a request is made through Alexa to switch Doorbell Alerts on or off, this information is stored in a context flow, (see next secion). The config anouncement for 'on' devices function iterates through this object and prepares a payload for the Alexa Routine nodes that determines to which Echo devices announcements are made. Line 27 of the function is where the spoken string is configured, and can be changed as preferred.

I elected to send both a sound effect and spoken phrase as I found it a little odd to have Alexa just pipe up out of the blue. The 3s delay before the spoken part is necessary to allow time for the effect to play and complete.


MANAGE ALEXA EVENTS

This part of the flow deals with Alexa device setup and command management.

Here I set up two devices named Doorbell Alert and Doorbell Alerts. I have a poor memory for infrequently used Alexa commands, and she is fussy about accuracy so I found this gives me more flexibility. You ca see here that, once discovered, these show up in the app as normal lights;



When deploying this flow for the first time, it's necessary to use the Alexa app to discover these devices. Otherwise they won't respond to commands.

Thereafter, the phrase 'Alexa, turn [on / off ] Doorbell Alerts' will trigger this part of the flow. If you want to use this flow yourself, but need to change these device names, note that after a change, its necessary to re-discover these devices in the Alexa app, and likely delete the old ones, if previously discovered.

The next part of this flow was the most tricky. The incoming payload generated from the user utterance contains a deviceID, but this cannot be used to address return spoken phrases back to the Echo. The Echo device name is required.

Therefore, it's necessary to use the Alexa Other node to get a list of recent Echo activity. This object contains details of up to 20 recent conversations with any Alexa device on the network. The Get Device Name of most recent Doorbell request function node then parses this object to find the most recent command that includes the substring 'doorbell alert'. If found, the device name can be retrieved.

This is likely the most vulnerable part of this solution. In a (very!) busy household, there's a risk that the relevant command will disappear from the activity list before it can be processed. Or indeed, if a doorbell alert command is spoken simultaneously into two or more devices, things will likely go awry. The short delay before the Get Activities node is critical as it takes a beat for the command to show in the recent activity list.

If you've changed the names of the device nodes at the start of this flow, it would also be necessary to amend line 11 of the 'Get Device Name of most recent Doorbell request' function to ensure the activity can be identified correctly.

All being well, the device name and current status is added to an array saved to a context variable and a response is composed and sent to the relevant Echo device indicating the notification status.



The Utility Functions section of the flow is just a couple of functions that help with management and debugging. 



The list devices trigger will compile a .msg that contains the currently stored list of devices that have registered an interest in receiving alerts along with the current on/off status flag for each.

The DELETE devices trigger will clear this stored list. A reset of sorts.

The flow Doorbell Alerts debug node also lives here, and is connected to other parts of the flow via link nodes.


If you wat to play along at home, please note that this solution has dependencies on the following nodes that should be installed before importing and deploying the flow.

This last one is a bit finicky to set up. It's necessary to authenticate your Alexa account the first time you use it. There are instructions on the repository page, but from memory, I seem to recall it being somewhat obtuse. Patience is a virtue here.

Before downloading the flow, if you find this interesting or useful, please do consider buying me a coffee. Every little helps feed my often insatiable tech. appetite!

Buy Me a Coffee at ko-fi.com


NB: There's now an updated version of this with some enhancements. Details here.


Anyhow, here's the flow. If you spot ay errors, please do leave a comment and I'll try to address. Thanks for looking!

[{"id":"55793b5be2d1c883","type":"tab","label":"Doorbell","disabled":false,"info":"","env":[]},{"id":"d24f7050.8bd5b","type":"inject","z":"55793b5be2d1c883","name":"Start listening","props":[],"repeat":"86400","crontab":"","once":true,"onceDelay":"5","topic":"","x":220,"y":100,"wires":[["97d0eb18.a1ee08"]]},{"id":"97d0eb18.a1ee08","type":"change","z":"55793b5be2d1c883","name":"","rules":[{"t":"set","p":"action","pt":"msg","to":"start","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":400,"y":100,"wires":[["e9458eaa.d45b4"]]},{"id":"55e40eba.19775","type":"inject","z":"55793b5be2d1c883","name":"Stop listening","repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":210,"y":140,"wires":[["3b192f32.da0b"]]},{"id":"3b192f32.da0b","type":"change","z":"55793b5be2d1c883","name":"","rules":[{"p":"action","pt":"msg","t":"set","to":"stop","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":400,"y":140,"wires":[["e9458eaa.d45b4"]]},{"id":"e9458eaa.d45b4","type":"onvif-events","z":"55793b5be2d1c883","name":"","deviceConfig":"fe013234.748d6","action":"","x":590,"y":100,"wires":[["b8a6fb75.d6bc28"]]},{"id":"b8a6fb75.d6bc28","type":"switch","z":"55793b5be2d1c883","name":"","property":"topic","propertyType":"msg","rules":[{"t":"eq","v":"RuleEngine/MyRuleDetector/Visitor","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":730,"y":100,"wires":[["9526130c.79b8f"]]},{"id":"9526130c.79b8f","type":"switch","z":"55793b5be2d1c883","name":"","property":"data.value","propertyType":"msg","rules":[{"t":"true"}],"checkall":"true","repair":false,"outputs":1,"x":850,"y":100,"wires":[["4cf4791f.5d21d8"]]},{"id":"4cf4791f.5d21d8","type":"delay","z":"55793b5be2d1c883","name":"","pauseType":"rate","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"5","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":true,"outputs":1,"x":1000,"y":100,"wires":[["e562683462597b36"]]},{"id":"dec68ffe455db698","type":"alexa-remote-other","z":"55793b5be2d1c883","name":"","account":"b9d166b16d287937","config":{"option":"get","value":{"what":"activities","count":{"type":"num","value":"1"},"offset":{"type":"num","value":"1"}}},"x":530,"y":500,"wires":[["454de8f65d88278a"]]},{"id":"454de8f65d88278a","type":"function","z":"55793b5be2d1c883","name":"Get Device Name of most recent Doorbell request","func":"let deviceName = null;\n\n// Iterate through the array of activities\nfor (let i = 0; i < msg.payload.length; i++) {\n    const activity = msg.payload[i];\n\n    // Check if the activity contains voiceHistoryRecordItems\n    if (activity.data && activity.data.voiceHistoryRecordItems) {\n        for (const item of activity.data.voiceHistoryRecordItems) {\n            // Check if transcriptText exists and contains the substring \"doorbell notification\"\n            if (item.transcriptText && item.transcriptText.includes(\"doorbell alert\")) {\n                // Found the desired text, get the deviceName\n                deviceName = activity.data.device.deviceName;\n                break;\n            }\n        }\n    }\n\n    // If deviceName is found, exit the loop\n    if (deviceName) {\n        break;\n    }\n}\n\n// Set the deviceName as the result\nmsg.deviceName = deviceName;\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":830,"y":500,"wires":[["529424166f5fec67","7536e089c87482f9"]]},{"id":"5e8d6a337df14589","type":"function","z":"55793b5be2d1c883","name":"store switch state","func":"// Check the payload value\nif (msg.payload === \"on\" || msg.payload === \"off\") {\n    // Store the payload value in a flow variable called \"switchState\"\n    flow.set(\"switchState\", msg.payload);\n    return msg;\n} else {\n    // If the payload value is not \"on\" or \"off\", you can handle it here if needed\n    // For example, you can send an error message or perform other actions\n    node.error(\"Invalid payload value: \" + msg.payload);\n}","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":610,"y":400,"wires":[["9c7e92e0a11cb2d2"]]},{"id":"529424166f5fec67","type":"function","z":"55793b5be2d1c883","name":"save to context variable","func":"// Retrieve the deviceName from the incoming msg object\nconst deviceName = msg.deviceName;\n\n// Retrieve the switchState from a flow variable\nconst switchState = flow.get(\"switchState\");\n\nif (!deviceName || !switchState) {\n    // Handle invalid data or missing values\n    msg.successStatus = 0; // Unsuccessful\n    msg.error = \"Invalid deviceName or switchState.\";\n} else {\n    // Prepare the data to be stored in context variables\n    // Use the deviceName as the key (unique identifier)\n    const deviceStatus = {\n        deviceName: deviceName,\n        switchState: switchState\n    };\n\n    // Retrieve the existing device statuses from flow context (assuming it's an object)\n    const existingDeviceStatuses = flow.get(\"deviceStatuses\") || {};\n\n    // Update the device status data with the deviceName as the key\n    existingDeviceStatuses[deviceName] = deviceStatus;\n\n    // Store the updated device status data in flow context\n    flow.set(\"deviceStatuses\", existingDeviceStatuses);\n\n    // Set the successStatus flag to 1 (successful)\n    msg.successStatus = 1;\n\n    // Include switchState in the msg payload\n    msg.switchState = switchState;\n}\n\nreturn msg;\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":410,"y":600,"wires":[["b994947ab60e8c90"]]},{"id":"f7fe5c7996fb6604","type":"function","z":"55793b5be2d1c883","name":"Make Response","func":"\n// Retrieve the switchState from the message payload\nconst switchState = msg.switchState;\n\n// Define a variable to store the result of the branching\nlet branchResult;\n\n// Check the value of switchState and branch accordingly\nif (switchState === \"on\") {\n    // If switchState is \"on\", do something (e.g., set a variable or perform an action)\n\n    msg.payload = {\n        \"text\": \"I will announce doorbells on this device\",\n        \"device\": msg.deviceName\n    }\n    branchResult = \"Switch is ON\";\n} else if (switchState === \"off\") {\n    // If switchState is \"off\", do something else (e.g., set a different variable or perform a different action)\n    msg.payload = {\n        \"text\": \"Doorbells wont announce on this device\",\n        \"device\": msg.deviceName\n    }\n    \n    branchResult = \"Switch is OFF\";\n} else {\n    // Handle other cases if needed\n    msg.payload = {\n        \"text\": \"oops. I've lost track of things. Please try that again.\",\n        \"device\": msg.deviceName\n    }\n\n\n    branchResult = \"Switch is in an unknown state\";\n}\n\n// You can use branchResult or set other variables based on the branch\nmsg.branchResult = branchResult;\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":800,"y":600,"wires":[["06e0fb3d494c9327"]]},{"id":"ad02a0a0c945f18f","type":"alexa-remote-routine","z":"55793b5be2d1c883","name":"","account":"b9d166b16d287937","routineNode":{"type":"speak","payload":{"type":"regular","text":{"type":"msg","value":"payload.text"},"devices":{"type":"msg","value":"payload.device"}}},"x":1160,"y":600,"wires":[[]]},{"id":"b994947ab60e8c90","type":"switch","z":"55793b5be2d1c883","name":"Success?","property":"successStatus","propertyType":"msg","rules":[{"t":"eq","v":"1","vt":"num"}],"checkall":"true","repair":false,"outputs":1,"x":620,"y":600,"wires":[["f7fe5c7996fb6604"]]},{"id":"06e0fb3d494c9327","type":"delay","z":"55793b5be2d1c883","name":"","pauseType":"delay","timeout":"1","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":980,"y":600,"wires":[["ad02a0a0c945f18f"]]},{"id":"c4bd9e0a5d16d339","type":"inject","z":"55793b5be2d1c883","name":"list devices","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":220,"y":780,"wires":[["bed345588330fe4b"]]},{"id":"bed345588330fe4b","type":"function","z":"55793b5be2d1c883","name":"Retrieve Device Statuses","func":"// Retrieve the deviceStatuses from the flow context\nconst deviceStatuses = flow.get(\"deviceStatuses\");\n\nif (deviceStatuses) {\n    // Extract all device names and switch states from the deviceStatuses object\n    const deviceInfo = Object.keys(deviceStatuses).map((deviceName) => ({\n        deviceName: deviceName,\n        switchState: deviceStatuses[deviceName].switchState\n    }));\n\n    if (deviceInfo.length > 0) {\n        // Set the list of device names and switch states in the msg.payload\n        msg.payload = deviceInfo;\n    } else {\n        // If no device names are found, set an informative message\n        msg.payload = \"No device names found.\";\n    }\n} else {\n    // If deviceStatuses is not found, set an informative message\n    msg.payload = \"No device statuses found.\";\n}\n\nreturn msg;\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":430,"y":780,"wires":[[]]},{"id":"d9f51aadc666bdfa","type":"debug","z":"55793b5be2d1c883","name":"Doorbell Alerts","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1160,"y":800,"wires":[]},{"id":"d4feba066cce7b62","type":"inject","z":"55793b5be2d1c883","name":"DELETE devices","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":200,"y":840,"wires":[["80f49f79c09a0969"]]},{"id":"80f49f79c09a0969","type":"function","z":"55793b5be2d1c883","name":"Clear Devices & Statuses","func":"\n// Clear the deviceStatuses variable in the flow context\nflow.set(\"deviceStatuses\", {});\n\n// Set a successStatus flag to indicate that the reset was successful (you can customize this as needed)\nmsg.successStatus = 1;\n\nreturn msg;\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":430,"y":840,"wires":[["bed345588330fe4b"]]},{"id":"df33c004d8f22261","type":"inject","z":"55793b5be2d1c883","name":"test announce","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":350,"y":200,"wires":[["e562683462597b36"]]},{"id":"e562683462597b36","type":"function","z":"55793b5be2d1c883","name":"config anouncement for 'on' devices","func":"// Retrieve the deviceStatuses from the flow context\nconst deviceStatuses = flow.get(\"deviceStatuses\");\n\nif (deviceStatuses) {\n    // Initialize an array to store device names with 'on' status\n    const devicesOn = [];\n\n    // Iterate through the deviceStatuses\n    for (const deviceName in deviceStatuses) {\n        if (deviceStatuses.hasOwnProperty(deviceName)) {\n            const switchState = deviceStatuses[deviceName].switchState;\n\n            // Check if the switch state is 'on' (you can customize this condition)\n            if (switchState === 'on') {\n                // Add the device name to the list of devices with 'on' status\n                devicesOn.push(deviceName);\n            }\n        }\n    }\n\n    if (devicesOn.length > 0) {\n        // Create the payload in the required format\n        const payload = {\n            type: 'speak',\n            payload: {\n                type: 'regular',\n                text: \"There's Someody at the door\",\n                devices: devicesOn\n            }\n        };\n\n        // Set the payload in the msg object\n        msg.payload = payload;\n    } else {\n        // If no devices are found with 'on' status, set an informative message\n        msg.payload = 'No devices are currently switched on.';\n    }\n} else {\n    // If deviceStatuses is not found, set an informative message\n    msg.payload = 'No device statuses found.';\n}\n\nreturn msg;\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":620,"y":200,"wires":[["d8acdcd07b4e928d"]]},{"id":"9c7e92e0a11cb2d2","type":"delay","z":"55793b5be2d1c883","name":"","pauseType":"delay","timeout":"1","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":360,"y":500,"wires":[["dec68ffe455db698"]]},{"id":"446290af56c8e2f7","type":"alexa-remote-routine","z":"55793b5be2d1c883","name":"","account":"b9d166b16d287937","routineNode":{"type":"speak","payload":{"type":"regular","text":{"type":"msg","value":"payload.payload.text"},"devices":{"type":"msg","value":"payload.payload.devices"}}},"x":1160,"y":260,"wires":[[]]},{"id":"d3c4ab189d5ed545","type":"alexa-remote-routine","z":"55793b5be2d1c883","name":"","account":"b9d166b16d287937","routineNode":{"type":"sound","payload":{"sound":{"type":"str","value":"amzn_sfx_doorbell_chime_02"},"devices":{"type":"msg","value":"payload.payload.devices"}}},"x":1160,"y":200,"wires":[[]]},{"id":"830b288c32f7bdd2","type":"delay","z":"55793b5be2d1c883","name":"","pauseType":"delay","timeout":"3","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":980,"y":260,"wires":[["446290af56c8e2f7"]]},{"id":"e5618282456c5d2f","type":"comment","z":"55793b5be2d1c883","name":"Utility Functions =========================","info":"","x":220,"y":720,"wires":[]},{"id":"8480fc71dcbdcba5","type":"comment","z":"55793b5be2d1c883","name":"Manage Alexa Events  ====================","info":"","x":220,"y":320,"wires":[]},{"id":"11a64ea38762b9fc","type":"comment","z":"55793b5be2d1c883","name":"Manage Reolink Events  ===================","info":"","x":220,"y":40,"wires":[]},{"id":"5048fa10bacc75cd","type":"amazon-echo-device","z":"55793b5be2d1c883","name":"Doorbell Alert","topic":"","x":380,"y":380,"wires":[["5e8d6a337df14589"]]},{"id":"b461a1c2abe68ed3","type":"amazon-echo-hub","z":"55793b5be2d1c883","port":"80","processinput":"0","discovery":true,"x":130,"y":400,"wires":[["5048fa10bacc75cd","c82d5479e642c179"]]},{"id":"c82d5479e642c179","type":"amazon-echo-device","z":"55793b5be2d1c883","name":"Doorbell Alerts","topic":"","x":380,"y":420,"wires":[["5e8d6a337df14589"]]},{"id":"d8acdcd07b4e928d","type":"switch","z":"55793b5be2d1c883","name":"","property":"payload","propertyType":"msg","rules":[{"t":"cont","v":"No device","vt":"str"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":850,"y":200,"wires":[["49d6f2fb0b1c1cce"],["830b288c32f7bdd2","d3c4ab189d5ed545"]]},{"id":"9896d0ca01888104","type":"link in","z":"55793b5be2d1c883","name":"doorbell - debug","links":["49d6f2fb0b1c1cce","7536e089c87482f9"],"x":985,"y":800,"wires":[["d9f51aadc666bdfa"]]},{"id":"49d6f2fb0b1c1cce","type":"link out","z":"55793b5be2d1c883","name":"dorbell debug call 1","mode":"link","links":["9896d0ca01888104"],"x":1095,"y":160,"wires":[]},{"id":"7536e089c87482f9","type":"link out","z":"55793b5be2d1c883","name":"dorbell debug call 2","mode":"link","links":["9896d0ca01888104"],"x":1095,"y":500,"wires":[]},{"id":"11fc376f018a4408","type":"comment","z":"55793b5be2d1c883","name":"ABOUT v1.0","info":"This flow is intended to allow Echo devices\nbe configured to announce visitors who\npress on an OnVif compatible doorbell,\nlike a ReoLink.\n\nFor full details about how this all works,\nsee:\nhttps://mediaserver8.blogspot.com/2023/11/announce-reolink-doorbell-on-echo.html\n\nIf you find this interesting or useful,\nplease consider conributig to my coffee\nfund here: \nhttps://ko-fi.com/8d14a771a245ac7","x":1170,"y":40,"wires":[]},{"id":"fe013234.748d6","type":"onvif-config","xaddress":"192.168.1.135","port":"8000","timeout":"10","checkConnectionInterval":"5","name":"Doorbell"},{"id":"b9d166b16d287937","type":"alexa-remote-account","name":"","authMethod":"proxy","proxyOwnIp":"192.168.1.199","proxyPort":"4567","cookieFile":"/usr/cookie.txt","refreshInterval":"3","alexaServiceHost":"alexa.amazon.co.uk","amazonPage":"amazon.co.uk","acceptLanguage":"en-UK","onKeywordInLanguage":"on","userAgent":"","autoInit":"on"}]



No comments: