Trigger the Automatic Cat Laser Pointer Toy Remotely
You made it here to part four because pushing that button isn’t enough. No, you want to trigger this cat laser pointer toy remotely, or even on a schedule. Well you’re in luck, because I too wanted more ways to do the same thing.
Part one got us on our way, part two was all about building the laser contraption, and part three brought it life. That means you have a fully functional cat laser pointer toy capable of being triggered at the push of a button.
What are we going to be doing here exactly?
- Set up your Gmail account with 2-step authentication for a secure way to interact with it through code.
- Introduce a modified version of the
GmailWrapper.py
script from my automatic cat feeder series to scan for an email with a given subject and, when found, trigger the cat laser pointer toy. - Use IFTTT to provide support for remote triggering and scheduling.
Enough said. It’s go time.
Preparing Your Gmail Account with 2-step Authentication
We’re going to be using email as the trigger to engage the cat laser pointer toy. Before that’s possible we’ll need a way to securely log into our Gmail account from code. You could store your credentials in plain text within the code we write, but I strongly discourage that. Instead, make use of Gmail’s two-step authentication and app passwords.
I created a new Gmail account for this purpose.
- Log into your Gmail account
- Navigate to the Sign-in and security page
- Under the Signing in to Google section, click the 2-Step Verification menu, then follow the instructions to enable 2-Step Verification
- Back in the Sign-in and security page right under the 2-Step Verification button you’ll see App passwords.
- Generate a password
- Note: this password will be 16 digits with spaces separating every 4 digits (e.g. xxxx xxxx xxxx xxxx). Please be sure to include the spaces!
- You can only view a generated password once, so copy it to the side for now.
The password you generated can be used to log into your Gmail account. We don’t need it right this second, but we’ll be using it to scan for an email that demands we initiate the laser sequence!
Writing Code to Read A Gmail Account
Now that we have a Gmail account ready to rock, let’s write some code to interrogate it. We’re going to be using the IMAPClient python library for this purpose, and we’ll wrap the calls to this client in our own Python class.
Install IMAPClient now from the terminal: sudo pip install imapclient
Create the GmailWrapper.py Script
Now let’s create our Gmail wrapper class: create a new file called GmailWrapper.py
with the following code:
#!/usr/bin/env python from imapclient import IMAPClient, SEEN SEEN_FLAG = 'SEEN' UNSEEN_FLAG = 'UNSEEN' class GmailWrapper: def __init__(self, host, userName, password): # force the user to pass along username and password to log in as self.host = host self.userName = userName self.password = password self.login() def login(self): print('Logging in as ' + self.userName) server = IMAPClient(self.host, use_uid=True, ssl=True) server.login(self.userName, self.password) self.server = server # The IMAPClient search returns a list of Id's that match the given criteria. # An Id in this case identifies a specific email def getIdsBySubject(self, subject, unreadOnly=True, folder='INBOX'): # search within the specified folder, e.g. Inbox self.setFolder(folder) # build the search criteria (e.g. unread emails with the given subject) self.searchCriteria = [UNSEEN_FLAG, 'SUBJECT', subject] if(unreadOnly == False): # force the search to include "read" emails too self.searchCriteria.append(SEEN_FLAG) # conduct the search and return the resulting Ids return self.server.search(self.searchCriteria) def getIdsByGmailSearch(self, search, folder='INBOX'): # powerful search enabled by Gmail. Examples: `in:unread, subject: <subject>` self.setFolder(folder) return self.server.gmail_search(search) def getFirstSubject(self, mailIds, folder='INBOX'): self.setFolder(folder) data = self.server.fetch(mailIds, ['ENVELOPE']) for msgId, data in data.items(): envelope = data[b'ENVELOPE'] return envelope.subject return None def markAsRead(self, mailIds, folder='INBOX'): self.setFolder(folder) self.server.set_flags(mailIds, [SEEN]) def setFolder(self, folder): self.server.select_folder(folder)
Verifying the GmailWrapper.py Class Works
Let’s do a test: our script is going to log into the Gmail account, search for email with a specific subject, retrieve that subject, then mark the email as read. Before running the code, send yourself an email with the subject begin laser ignition (the search is case-insensitive, by the way).
We’re going to use the Python interpreter to run our tests. In your terminal make sure you’re in the same directory as the GmailWrapper.py
script we just created, then:
# press enter after each line for the interpreter to engage # invoke the interpreter python # import our wrapper class for reference from GmailWrapper import GmailWrapper # create an instance of the class, which will also log us in # the <password> should be the 2-step auth App Password, or your regular password gmailWrapper = GmailWrapper('imap.gmail.com', '<your gmail username>', '<password>') # search for any unread emails with the subject 'begin laser ignition', and return their Ids ids = gmailWrapper.getIdsByGmailSearch('begin laser ignition') # have the interpreter print the ids variable so you know you've got something ids # grab the full subject of the first id returned subject = gmailWrapper.getFirstSubject(ids) # have the interpreter print the subject variable to see what you've got subject # we successfully found and read our email subject, now lets mark the email as read gmailWrapper.markAsRead(ids) # exit the interpreter quit()
If everything went as planned your email should now be marked as read. Pretty neat!
LaserWrapper, Meet GmailWrapper: Putting the Two Together
As we know by now, the LaserWrapper.py
script is constantly watching the physical button and waiting for it to be pressed. We’re going to modify it a bit so it also watches the Gmail account and waits for an email with the correct subject to come through. In the event it finds this email we want it to trigger the cat laser pointer toy.
Alright, time for some modifications. Open your LaserWrapper.py
script and replace its contents with the following:
#!/usr/bin/env python from Laser import Laser from GmailWatcher import GmailWrapper import json import datetime import RPi.GPIO as GPIO import time from imaplib import IMAP4 HOSTNAME = 'imap.gmail.com' USERNAME = 'your username' PASSWORD = 'your password' # seconds to wait before searching Gmail CHECK_DELAY = 30 # seconds to wait before logging into gmail. if we don't wait, we run the risk of trying to # log in before the Pi had a chance to connect to wifi. GMAIL_CONNECT_DELAY = 20 # minutes to wait before reconnecting our Gmail instance. GMAIL_RECONNECT_DELAY = 60 GPIO_BUTTON = 26 FIRE_LASER_SUBJECT = 'begin laser ignition' DISMANTLE_LASER_SUBJECT = 'dismantle laser' engage = False gmailWrapper = None laser = Laser() start_time = datetime.datetime.now() run_time = 0 last_gmail_check_time = datetime.datetime.now() last_gmail_connect_attempt = datetime.datetime.now() last_reconnect_attempt = datetime.datetime.now() default_configuration = '{"run_time": 30, "min_movement": 12, "x_min": 0, "x_max": 90, "y_min": 0, "y_max": 22}' def initiateLaserSequence(): global gmailWrapper global engage GPIO.setmode(GPIO.BCM) GPIO.setup(GPIO_BUTTON, GPIO.IN, pull_up_down=GPIO.PUD_UP) stop = False run = True # wire up a button press event handler to avoid missing a button click while the loop below # is busy processing something else. GPIO.add_event_detect(GPIO_BUTTON, GPIO.FALLING, callback=__button_handler, bouncetime=200) last_gmail_check = datetime.datetime.now().time() print 'running, press CTRL+C to exit...' try: while run: try: __check_gmail_connection() # avoid pinging gmail too frequently so they don't lock us out. if(__should_check_gmail(CHECK_DELAY)): print 'Checking Gmail!' stop = __should_stop_firing() ids = __get_email_ids_with_subject(FIRE_LASER_SUBJECT) if(len(ids) > 0): print 'Email found, initiating laser sequence!' engage = True laser.stop() # grab any config options from the email and begin __calibrate_laser(__get_configuration(ids)) gmailWrapper.markAsRead(ids) if(stop): engage = False stop = False laser.stop() if(engage): if(__run_time_elapsed()): stop = True else: laser.fire() else: # sleep here to lessen the CPU impact of our infinite loop # while we're not busy shooting the laser. Without this, the CPU would # be eaten up and potentially lock the Pi. time.sleep(1) except IMAP4.abort, e: # Gmail will expire your session after a while, so when that happens we # need to reconect. Setting None here will trigger reconnect on the # next loop. gmailWrapper = None print 'IMAP4.abort exception: {0}'.format(str(e)) except Exception, e: # swallowing exceptions isn't cool, but here we provide an opportunity to # print the exception to an output log, should crontab be configured this way # for debugging. print 'Unhandled exception: {0}'.format(str(e)) except KeyboardInterrupt: run = False print 'KeyboardInterrupt: user quit the script.' break finally: print 'Exiting program' laser.stop() GPIO.cleanup() def __button_handler(channel): global engage print 'Button pressed! '.format(str(channel)) if(engage): print 'Already firing the laser, button press ignored' else: print 'Initiating Firing Sequence!' # only start a new firing sequence if we're not already in the middle of one. engage = True __calibrate_laser(None) def __check_gmail_connection(): if(gmailWrapper is None): __connect_gmail() def __connect_gmail(): global gmailWrapper global last_gmail_connect_attempt now = datetime.datetime.now() next_connect_time = (last_gmail_connect_attempt + datetime.timedelta(seconds=GMAIL_CONNECT_DELAY)).time() if(now.time() > next_connect_time): print '__connect_gmail: Attempting to login to Gmail' try: last_gmail_connect_attempt = now gmailWrapper = GmailWrapper(HOSTNAME, USERNAME, PASSWORD) except Exception, e: print '__connect_gmail: Gmail failed during login, will retry automatically.'.format(str(e)) def __should_check_gmail(delay): global last_gmail_check_time if(gmailWrapper is None): # we haven't yet successfully connected to Gmail, so exit return now = datetime.datetime.now() next_check_time = (last_gmail_check_time + datetime.timedelta(seconds=delay)).time() if(now.time() > next_check_time): last_gmail_check_time = now return True return False def __run_time_elapsed(): now = datetime.datetime.now() end_time = (start_time + datetime.timedelta(seconds=run_time)).time() if(now.time() > end_time): return True return False def __calibrate_laser(configuration): global start_time global run_time if(configuration is None): # no user defined config, so we'll go with the defaults configuration = json.loads(default_configuration) print "starting laser with config: {0}".format(configuration) start_time = datetime.datetime.now() run_time = configuration.get('run_time') min_movement = configuration.get('min_movement') x_max = configuration.get('x_max') x_min = configuration.get('x_min') y_max = configuration.get('y_max') y_min = configuration.get('y_min') laser.calibrate_laser(min_movement, x_min, x_max, y_min, y_max) def __get_email_ids_with_subject(subject): return gmailWrapper.getIdsByGmailSearch('in:unread subject:{0}'.format(subject)) def __get_configuration(emailIds): subject = gmailWrapper.getFirstSubject(emailIds) config_start_index = subject.find('{') # no config found in subject, so return nothing if(config_start_index == -1): return None # grab the substring from opening { to the end return json.loads(subject[config_start_index:None]) def __should_stop_firing(): ids = __get_email_ids_with_subject(DISMANTLE_LASER_SUBJECT) return len(ids) > 0 if __name__ == '__main__': initiateLaserSequence()
The above script is almost ready for use, we just need to make a few tweaks:
- Replace lines
12 and 13
with your Gmail username (that’s your email without @gmail), and the app password you generated. - Replace lines
26 and 27
with the email subjects that you want to use to start or stop the laser sequence. That’s right, the ability to remotely stop the laser is baked in as well, just in case it’s running a bit longer than desired.
Once these changes are in place, give it a test drive. Run the script and press the physical button to ensure we didn’t break anything there. If all looks good, send yourself an email with the subject you chose (I used begin laser ignition
). After about 30 seconds the laser should have began firing.
Subject Matters: Override the Default Configuration
At this point we’re able to trigger the cat laser pointer toy remotely by sending an email. This is pretty powerful stuff. If you recall from the previous posts, we built in the ability to configure the cat laser pointer toy in the form of a JSON
string. What if we want to set the run time, or the minimum amount of movement, remotely?
You’re in luck! The script we just wrote above is smart enough to do that. If you send an email with just the trigger key in the subject (e.g. begin laser ignition
), then it will use the default_configuration
variable on line 39 of the script. However, you can override that by adding your own configuration into the subject itself.
Here’s a sample email subject to showcase what I mean:
begin laser ignition {"run_time": 30, "min_movement": 12, "x_min": 0, "x_max": 90, "y_min": 0, "y_max": 22}
The script we wrote will parse this more complex subject and use the configuration settings you provide here over the default ones.
Scheduling an Email to be Sent Regularly
We have the code ready to rock. Sending an email on demand will cause the cat laser pointer toy to start firing, but what about firing it on a schedule? That’s where we’ll make use of IFTTT. IFTTT stands for if this then that and allows you to connect two disparate systems based on what they call a “recipe” (trigger and action). For our purpose, we need the clock to trigger an email to be sent (action).
Here’s what to do:
- Setup an account if you haven’t already (free, free, free).
- Use IFTTT website or download the app to your phone and log in.
- In the My Applets section, add a new applet.
- You’ll see a screen saying if +this then that: click the +this button, then select the Date & Time service.
- Select the Every day at trigger, and select the time you’d like the cat laser pointer toy to activate, then hit next
- Now you’ll see +that, click it and find the Gmail service. You’ll need to connect the service to your Gmail account. Once finished, set the action to Send yourself an email with the subject Begin Laser Ignition.
- Don’t forget, you have the option of adding configuration to the subject, too.
- Hit next, then Finish
There you have it, every day at the time you specified an email will be sent to your account. If you had any issues setting up the IFTTT recipe, check out this post for a really nice and in-depth walk-through.
Having fun? Here’s Other Ways to Trigger
Alexa
Alexa (and the Echo Dot) integrates nicely with IFTTT. In the IFTTT app, create a new recipe with the trigger (+this) connecting to Alexa. You’ll need to connect the service like you did for Gmail. Once connected, select the option to Say a specific phrase
and enter a phrase like cat laser
. Once the Alexa side is setup, set the action (+that) to send an email, like we did in the previous section.
Hands free laser initiation at the ready, just say: Alexa, trigger the cat laser.
The DO Button App
Created by the IFTTT team, the DO Button app and accompanying widget provides a straightforward way to trigger the action. You! You’re the trigger. You setup a recipe, same as before, except you’ll notice there’s no +this. You are +this. You open the app and click the button, it then triggers an email which triggers the cat laser. This app can also be configured to show on your iPhone or Androids home screen, so triggering the laser is even easier.
Conclusion
I hope you enjoyed this project as much as I did. It was a blast seeing a shark with a laser attached to its head causing mayhem throughout my apartment. I think the cats enjoy it too, almost as much as I do.
As always, a little shoutout to the previous sections in case you need to get there:
- Part one where we found out what we were building and what it would take.
- Part two which helped guide us to creating a machine of chaos.
- Part three where our inner mad scientist gave life to this machine in the form of code.
- And of course, the bonus part, part four where we couldn’t live without a remote way to control our cat laser pointer toy.
I encourage you to leave feedback in the comments below. If you’re stuck, reach out! And of course, thank you for reading.
Hi, I’m Sam.
I’m a programmer and a DIYer. When I’m not finding things to build I enjoy cooking, hiking, camping and traveling the world with my best friend. Say Hello!