Cloudreach Blog

Catch the latest stuff

IoT Doorbell

Neil Stewart 16th June 2017
IoT Doorbell

I’ve been a bit obsessed with Home automation, Serverless and IoT for a wee while and I find that these are three technologies that complement each other super well. I started my IoT collection about 2 years ago when I asked for Philips Hue lights for Christmas… I got a few confused comments and looks when I first mentioned that I wanted light bulbs as a gift but being the geek that I am, I ignored the “haters”.  2 years and 15 bulbs later, my collection has expanded to multiple Sonos systems, Apple HomeKit, Amazon Alexa and even a smart Kettle. I think it is fair to say that I am invested in all things IoT. So now we have the origin of my obsession out the way… let’s find out about this IoT Doorbell.

Creating an IoT Doorbell

During a 3 month visit to New York, Cloudreach moved offices and we realised something was missing – the doorbell. Rather than getting a normal doorbell for visitors when they got to our floor, I put my hat in the ring to make something more cloudy. With access to the necessary tech, it was a simple thing to get started.

If you would like to build your own Cloudy IoT doorbell, the solution was achieved with the following pieces.

IoT Doorbell Ingredients

  1. AWS IoT Button – the trigger
  2. AWS Lambda – the Cloud processor
  3. Cloudreach Sceptre – the Cloud deployer
  4. AWS SQS – For doorbell action queuing
  5. Sonos speakers – The bell
  6. AWS Polly – Doorbell tone generation
  7. AWS S3 – Storage for the tones and Lambda code
  8. Raspberry Pi – Cloud-to-LAN-to-Sonos bridge

Rough Cost

£250 😜  The cost is obviously high here if you wanted to buy everything new. However, the point of this article is to show you how you can use all of these components together. I’ve ignored AWS costs as you could run this in a free-tier account easily.

Method

So, to get this all working, I first configured the IoT button to trigger a Lambda function. Setup was simple and involved uploading a key to the IoT button through a web interface it makes available through its own WiFi network (pretty cool!). Once configured, adding the button as a trigger to a specific lambda function is easy!

Next, Lambda. In this solution, Lambda does very little – All it does is put an item in a doorbell SQS queue. The queue is configured so messages won’t live past 1 minute. This prevents the doorbell being spammed if it is offline on the local side and comes back up to lots of “ding dong” requests.

Code for Lambda

"""Doorbell Lambda."""

import boto3

QUEUE = "YOUR QUEUE URL"


def doorbell_handler(e, c):  
    """Doorbell Handler."""
    print e
    sqs = boto3.client('sqs')

    sqs.send_message(
        QueueUrl=QUEUE,
        MessageBody="ding-dong",
    )

    print "Doorbell ring"

Pretty simple right? We now have a button that triggers a function and we end up with a message in the queue. Next up is the local network side of things.

Local ding dong

The Raspberry Pi is king here and its job is to get messages from SQS and then use a Python Sonos module to play various doorbell tones on the desired speaker. Some things to keep in mind here:

Stopping music to play a tone is great… leaving the room without music is really annoying. What this means is that the way you get the tone to play on the Sonos is to take a snapshot of the current queue, the current song and where it is (i.e how many seconds/minutes into the song is it). We then play our tone and then use the snapshot we took before to clear the tone from the queue, put the existing queue back in place and play the previous song from where it left off. Great!

The gotcha I found here was that it was not easy to know when the tone had finished playing and so it would mean you either replace the queue too early or too late. My solution to this was to set up a dictionary with each tone and its length. I would set the tone to play and then sleep for the length of the tone, then replace everything.

Playing music that was paused is also annoying. A queue of songs may be in place but not playing. Remembering the play/pause state is important here – Check what it is before playing the tone, then depending on what it was, once you replace the queue – perform the appropriate action. Leaving playing music paused and paused music playing is not great.

Multi-speaker support in its current state, multi-speaker does not work… This is because of the way Sonos speakers are grouped when using multi-room. From my observations, I noticed that 1 speaker becomes the “leader” in the room, and playing music to the wrong speaker causes the tone to play out of that speaker but not the rest. In the time I had to spend on this project, I didn’t really try to get around this as the speaker layout was such that there were 3 rooms each with 1 speaker and the main room where a bell would be useful had 1 of these speakers.

Moving on

So keeping what I have highlighted above in mind, let’s look at what the Raspberry Pi does here. I wrote a python script that does all the work on the local side of the doorbell. It will read messages from the SQS queue and then play a random tone from a pre-set selection. Including tone length and S3 locations. Once it plays the tone, it puts the state of Sonos back to how it was and starts over once it has had a 60-second nap – this prevents spamming the doorbell. After its wee sleep, it starts again – polling for messages and making ding dongs.

doorbell.py

#!/usr/bin/python
"""Python script to poll SQS queue for messages."""

import boto3  
import time  
import random  
import soco  
import logging  
import logging.handlers  
import argparse  
import sys

# Deafults
LOG_FILENAME = "/var/log/doorbell.log"  
LOG_LEVEL = logging.INFO  # Could be e.g. "DEBUG" or "WARNING"


BUCKET_NAME = "your s3 bucket"  
SQS_QUEUE = "your doorbell sqs"  
SONOS_NAME = "The room name to use "

# Define and parse command line arguments
parser = argparse.ArgumentParser(description="My simple Python service")  
parser.add_argument("-l", "--log", help="file to write log to (default '" + LOG_FILENAME + "')")

# If the log file is specified on the command line then override the default
args = parser.parse_args()  
if args.log:  
        LOG_FILENAME = args.log

# Configure logging to log to a file, making a new file at midnight and keeping the last 3 day's data
# Give the logger a unique name (good practice)
logger = logging.getLogger(__name__)  
# Set the log level to LOG_LEVEL
logger.setLevel(LOG_LEVEL)  
# Make a handler that writes to a file, making a new file at midnight and keeping 3 backups
handler = logging.handlers.TimedRotatingFileHandler(LOG_FILENAME, when="midnight", backupCount=3)  
# Format each log message like this
formatter = logging.Formatter('%(asctime)s %(levelname)-8s %(message)s')  
# Attach the formatter to the handler
handler.setFormatter(formatter)  
# Attach the handler to the logger
logger.addHandler(handler)  
# Make a class we can use to capture stdout and sterr in the log


class MyLogger(object):  
    """Logger Class."""

    def __init__(self, logger, level):
        """Need a logger and a logger level."""
        self.logger = logger
        self.level = level

    def write(self, message):
        """Write the log."""
        if message.rstrip() != "":
            self.logger.log(self.level, message.rstrip())

# Replace stdout with logging to file at INFO level
sys.stdout = MyLogger(logger, logging.INFO)  
# Replace stderr with logging to file at ERROR level
sys.stderr = MyLogger(logger, logging.ERROR)


def get_sonos(n):  
    """Get Sonos Player."""
    try:
        players = soco.discover()
        print players
        n = [x for x in players if x.player_name == n][0]
        return n
    except:
        return False


def play_doorbell():  
    """Play Doorbell."""
    name = SONOS_NAME
    player = get_sonos(name)

    if player is not False:
        # original_queue = player.get_queue()
        state = player.get_current_transport_info()
        track = player.get_current_track_info()
        volume = player.volume

        sounds = [
            {
                "name": "british-man.mp3",
                "duration": 5
            },
            {
                "name": "british.mp3",
                "duration": 5
            },
            {
                "name": "norweigen.mp3",
                "duration": 7
            }
        ]
        choice = random.choice(sounds)
        print choice

        if state['current_transport_state'] == 'PLAYING':
            player.pause()

        player.volume = 80
        player.play_uri("{}/sounds/{}".format(
            BUCKET_NAME,
            choice['name'])
        )

        time.sleep(choice['duration'])
        player.volume = volume

        queue = player.get_queue()
        if len(queue) > 0:
            player.play_from_queue(
                int(
                    track['playlist_position']
                ) - 1,
                False
            )

            player.seek(
                track['position']
            )
            if state['current_transport_state'] == 'PLAYING':
                player.play()
    else:
        print("Couldn't connect to sonos")


def process_message(message):  
    """Process message."""
    print(message.body)
    play_doorbell()
    message.delete()

while True:  
    try:
        s = boto3.session.Session(profile_name='which profile to use', region_name='a region')

        sqs = s.resource('sqs')
        queue = sqs.Queue(
            SQS_QUEUE
        )

        while True:
            try:
                print "Getting messages"
                m = queue.receive_messages(
                    WaitTimeSeconds=20
                )
                print(m)
                if len(m) > 0:
                    message = m[0]
                    process_message(message)

                    time.sleep(60)
                time.sleep(2)
            except Exception as e:
                print(e)
                time.sleep(60)
                continue

    except Exception as d:
        print(d)
        time.sleep(30)
        continue

This is tied together by setting up a service on the Pi that starts on boot and allows me to restart, stop and start when needed.

doorbell.sh

#!/bin/sh

### BEGIN INIT INFO
# Provides:          myservice
# Required-Start:    $remote_fs $syslog
# Required-Stop:     $remote_fs $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Put a short description of the service here
# Description:       Put a long description of the service here
### END INIT INFO

# Change the next 3 lines to suit where you install your script and what you want to call it
DIR=/usr/local/bin/doorbell  
DAEMON=$DIR/doorbell.py  
DAEMON_NAME=doorbell

# Add any command line options for your daemon here
DAEMON_OPTS=""

# This next line determines what user the script runs as.
# Root generally not recommended but necessary if you are using the Raspberry Pi GPIO from Python.
DAEMON_USER=root

# The process ID of the script when it runs is stored here:
PIDFILE=/var/run/$DAEMON_NAME.pid

. /lib/lsb/init-functions

do_start () {  
    log_daemon_msg "Starting system $DAEMON_NAME daemon"
    start-stop-daemon --start --background --pidfile $PIDFILE --make-pidfile --user $DAEMON_USER --chuid $DAEMON_USER --startas $DAEMON -- $DAEMON_OPTS
    log_end_msg $?
}
do_stop () {  
    log_daemon_msg "Stopping system $DAEMON_NAME daemon"
    start-stop-daemon --stop --pidfile $PIDFILE --retry 10
    log_end_msg $?
}

case "$1" in

    start|stop)
        do_${1}
        ;;

    restart|reload|force-reload)
        do_stop
        do_start
        ;;

    status)
        status_of_proc "$DAEMON_NAME" "$DAEMON" && exit 0 || exit $?
        ;;

    *)
        echo "Usage: /etc/init.d/$DAEMON_NAME {start|stop|restart|status}"
        exit 1
        ;;

esac  
exit 0

Altogether, this leaves you with a robust, expandable and cloudy doorbell. It is probably the most over engineered doorbell ever but it is a great example of IoT in AWS and how easy it is to get something together with a little tinkering.

Check out the working doorbell below!

I have applied the same Trigger -> Lambda -> SQS -> Pi flow to Alexa skills for controlling stuff at home that Alexa doesn’t yet support – such as playing & pausing Sonos & Chromecast (video here)

And that is it. Let me know what you think and see if you can get something similar working! Have already? Share it below in the comments 🙂

 

For more cloudy posts by Neil Stewart, click here