Blog
January 13, 2019 Marie H.

Smart Garage Door Primer

Smart Garage Door Primer

Photo by <a href="https://unsplash.com/@user_pascal?utm_source=cloudista&utm_medium=referral" target="_blank" rel="noopener">User_Pascal</a> on <a href="https://unsplash.com/?utm_source=cloudista&utm_medium=referral" target="_blank" rel="noopener">Unsplash</a>

To be honest I've been super busy lately among other things so going to keep this one short and simple and to the point. I've added a stupid amount of comments in the code below so anyone should be able to follow. But just hacking around with my wifi connected garage door there isn't really a official alexa skill or google action setup for it and started piecing one together myself. This isn't by any means complete but should help anyone working on a similar project.

Enjoy!

You will need Python 3.6+ for this code as it uses asyncio.

#!/usr/bin/env python
"""
In progress Lambda func for integration with
Alexa for MyQ Garage Door opening
"""
import asyncio
import logging
import sys
import uvloop

from aiohttp import ClientSession

import pymyq


# Setup some basic logging stuff
log_format = '%(asctime)-15s %(levelname)s %(message)s'
logging.basicConfig(format=log_format, level=logging.DEBUG)
logger = logging.getLogger('myq')


# Garage door states
VALID_STATES = [
    'open',
    'opening',
    'close',
    'closing',
]

# Actions a user can take
VALID_ACTIONS = [
    'open',
    'close',
]

CLOSED_STATE = 'closed'
OPENED_STATE = 'open'

def is_valid_state_change(current_state, desired_action):
    """
    Sanity checking on desired actions
    :param current_state: device.state
    :param desired_action: open/close
    :return: Boolean
    """
    # make sure something reasonable is asked for
    if desired_action not in VALID_ACTIONS:
        return False

    # you can only do one or the other buddy
    # based on the current state
    if desired_action == 'open':
        # must be closed
        if current_state == CLOSED_STATE:
            return True

    if desired_action == 'close':
        # must be open
        if current_state == OPENED_STATE:
            return True

    return False


def is_valid_brand(brand):
    """
    Ensure proper brand is being used before trying
    to auth
    :param brand: Brand of device
    :return: Boolean
    """
    VALID_BRANDS = [
        'chamberlain',
        'craftsman',
        'liftmaster',
        'merlin',
    ]
    if brand in VALID_BRANDS:
        return True
    logger.error("User supplied invalid branch: %s", brand)
    return False


async def init():
    """
    Creates http session to run
    commands
    """

    # add your auth here
    email = ''
    password = ''
    brand = ''

    # setup http session
    async with ClientSession() as session:
        if is_valid_brand(brand):

            # auth with myq api
            myq = await pymyq.login(
                email,
                password,
                brand,
                session
            )
            # myq methods:
            #     authenticate
            #     get_devices

            # get devices - returns array
            devices = await myq.get_devices()
            for device in devices:
                # available properties:
                #     'brand', 'device_id', 'name',
                #     'parent_id', 'serial', 'state', 'type'
                logger.debug('Brand: %s', device.brand)
                logger.debug('Device ID: %s', device.device_id)
                logger.debug('Name: %s', device.name)
                logger.debug('Parent ID: %s', device.parent_id)
                logger.debug('Serial Number: %s', device.serial)
                logger.debug('State: %s', device.state)
                logger.debug('Type: %s', device.type)

                # prompt the user
                try:
                    desired_action = input('Would you like to open or close the door? ')
                except KeyboardInterrupt:
                    logger.info("Exiting cleanly; have a good day!")
                    sys.exit(0)

                if is_valid_state_change(device.state, desired_action):
                    if desired_action == 'open':
                        status = await device.open()
                    if desired_action == 'close':
                        status = await device.close()

                    if status:
                        logger.info("Performing requested action: %s", desired_action)
                    else:
                        logger.error("Received a invalid response from the server: %s", status)

                else:
                    logger.error("Sorry, that was not a valid option based on the current state of your machine")


if __name__ == '__main__':
    asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
    loop = asyncio.get_event_loop()
    loop.run_until_complete(init())