Using the AWS IoT Button and Lambda to Receive On-Demand Daily Briefings

AWS IoT Button

Backstory

A little while ago, I impulse purchased one of Amazon’s $20 “AWS IoT Buttons”. These devices, which are really just souped-up and customizable versions of Amazon’s extremely popular Dash Buttons, allow developers to connect to and trigger actions within Amazon Web Services (AWS). As you might imagine - people much smarter than myself have found about a million things to do with these little guys: everything from ordering pizza and Sweetgreen to triggering IFTTT (If This Then That) actions.

At a very high level, the way the IoT Buttons work is that they register button clicks as events within AWS IoT. The events can then be used to trigger a Lambda function, which can instantaneously execute code in a number of languages (Node.js, Python, Java, etc). This code can call any number of external APIs or internal AWS services, meaning that there’s almost an infinite number of things you can program your IoT Button to do.

AWS IoT Process

Of course, faced with this limitless range of possibilities, I had a hard time deciding what to actually do with my IoT Button. I wanted it to fit a common use case in my life, but also didn’t want it to do something mundane, like turning off the lights, or something that had already been done, like ordering pizza. Ultimately, I decided to make my IoT Button into a “morning briefing” device - something I could click on my way out the door and get a notification with all the important information I needed to know for the day.

Button Setup

To get kickstarted, I downloaded the AWS IoT Button Developer application for my phone. Once installed, I used the app to register my Button to my AWS account and deploy the SMS notification demo application within Lambda. Once deployed, this application will send a text to your phone with the Button’s name and status.

Lambda Code

Using the SMS demo code as a starting point, I began building out my “daily briefing” code. Since the SMS message simply sends a plain text message, all I would need to do is modify the text string with my desired information before it is sent via SMS. However, I would need to construct some helper functions to pull down and embed that information. Here are the different services I ended up using:

  • Dark Sky API: weather forecasts
  • WMATA API: real-time Metro rail predictions
  • Capital Bikeshare API: Bikeshare station statuses
  • CoinMarketCap API: cryptocurrency price listings

Dark Sky

# Global Variables
DARK_SKY_KEY = '#####'
DARK_SKY_COORD = '38.9144,-77.0232'

# Download a JSON of weather data from Dark Sky at the given coordinates
def get_weather():
    weather = {}
    try:
        conn = http.client.HTTPSConnection('api.darksky.net')
        conn.request("GET", "/forecast/" + DARK_SKY_KEY + "/%s" % DARK_SKY_COORD)
        response = conn.getresponse()
        data = response.read()
        conn.close()
    except Exception as e:
        print("[Errno {0}] {1}".format(e.errno, e.strerror))
    parsed_json = json.loads(data)
    return parsed_json

# Return a string response for the current weather
def build_weather_blurb(weather):
    response = ""
    response += "Weather: " + weather["currently"]["summary"] + "\n"
    response += "Temperature: " + str(weather["currently"]["apparentTemperature"]) + "\u00b0\n"
    response += "Upcoming: " + weather["minutely"]["summary"] + "\n"
    response += "Today: " + weather["hourly"]["summary"] + "\n"
    return response

Metro

# Global Variables
METRO_API_KEY = '####'
METRO_STATION_ID = 'E04'
# Metro API URL parameters
headers = {
    # Using demo API key
    'api_key': METRO_API_KEY,
}
params = urllib.parse.urlencode({
})

# Get next 2 southbound trains at Shaw
def get_shaw_trains():
    trains = get_trains(METRO_STATION_ID)
    south_trains = sort_southbound_trains(trains)
    return return_southbound_trains(south_trains)

# Retrieve the incoming trains at a given station
def get_trains(station_id):
    try:
        conn = http.client.HTTPSConnection('api.wmata.com')
        conn.request("GET", "/StationPrediction.svc/json/GetPrediction/" + station_id + "?%s" % params, "{body}", headers)
        response = conn.getresponse()
        data = response.read()
        conn.close()
    except Exception as e:
        print("[Errno {0}] {1}".format(e.errno, e.strerror))
    parsed_json = json.loads(data)
    trains = parsed_json["Trains"]
    return trains

# Return the next 2 southbound trains at a given station
def sort_southbound_trains(trains):
    south_trains = []
    count = 0
    for train in trains:
        if train['Group'] == '2' and count < 2:
            south_trains.append(train)
            count += 1
    return south_trains

# Print the south bound trains at a given station
def return_southbound_trains(south_trains):
    south = ""
    if len(south_trains) == 0:
        return "\n"
    for train in south_trains:
        south += train['Min'] + " minutes to " + train['Destination'] + " (" + train['Line'] + ")\n"
    return south

Capital Bikeshare

# Global Variables
BIKESHARE_XML_PATH = '/tmp/bikes.xml'
BIKESHARE_STATIONS = ['145', '47']

# Download the latest XML of Capital Bikeshare data
def load_bikeshare_XML():
    url = 'https://feeds.capitalbikeshare.com/stations/stations.xml'
    data = requests.get(url)
    # Save the XML file into /tmp so Lambda can access it
    with open(BIKESHARE_XML_PATH, 'wb') as f:
        f.write(data.content)
       
# Return station information for stations: 145 (7th and R) and 47 (7th and T)
def parse_bikeshare_stations(xmlfile):
    # create element tree object
    tree = ET.parse(xmlfile)
    # get root element
    root = tree.getroot()
    selected_stations = []
    for s in root.findall("station"):
        station = {}
        for child in s:
            station[child.tag] = child.text
        if station["id"] in BIKESHARE_STATIONS:
            selected_stations.append(station)
    return selected_stations

# Return a string containing updates for the stations passed
def return_shaw_bikeshare_stations(list):
    response = ""
    for station in list:
        response += station['name'] + ": " + station["nbBikes"] + " bikes, " + station["nbEmptyDocks"] + " docks.\n"
    return response

CoinMarketCap

# Global Variables
COIN_LIST = ['BTC'] # Add whatever coins you want here, by symbol

# Download a JSON of crypto prices from CoinMarketCap
def get_coins():
    weather = {}
    try:
        conn = http.client.HTTPSConnection('api.coinmarketcap.com')
        conn.request("GET", "/v1/ticker/")
        response = conn.getresponse()
        data = response.read()
        conn.close()
    except Exception as e:
        print("[Errno {0}] {1}".format(e.errno, e.strerror))
    parsed_json = json.loads(data)
    return parsed_json

# Get list of coin symbols and prices in market cap order
def get_prices(coins, my_list):
    response = ""
    for coin in coins:
        if coin['symbol'] in my_list:
            response += coin['symbol'] + ": $" + coin['price_usd'] + "\n"
    return response

Tweaking the Lambda Handler

Once I added the different functions, all that was left to do was modify the main lambda_handler function to build a new outgoing SMS message with all my information. To do so, I commented out the original message content and added a new message string containing the outputs of all of my weather, Metro, Bikeshare, and crypto API calls.

# Global Variables
PHONE_NUMBER = '####'

# Triggered upon call of the Lambda function
def lambda_handler(event, context):
    logger.info('Received event: ' + json.dumps(event))
    #message = 'Hello from your IoT Button %s. Here is the full event: %s' % (event['serialNumber'], json.dumps(event))
    message = build_weather_blurb(get_weather()) + "\n" + get_shaw_trains() + "\n" + get_capital_bikeshare_status() + "\n" + get_prices(get_coins(), COIN_LIST)
    sns.publish(PhoneNumber=PHONE_NUMBER, Message=message)
    logger.info('SMS has been sent to ' + PHONE_NUMBER)

Finished Product

Woo! Now clicking our IoT Button triggers API calls to Dark Sky, Metro, Bikeshare, and CoinMarketCap, parses the responses, and sends an SMS with the information straight to my phone number. Take a peek at the screenshot below for what it looks like, and feel free to check out the first version of the code here.

Daily Briefing Text