Sunday, January 15, 2017

AWS IoT Button and TP-Link Smart Plug

A TP-Link smart plug is inserted into a standard electrical outlet and is controlled by the Kasa smartphone app to turn on and off the power to whatever device, for example a lamp, that’s plugged into it. It can also be controlled by voice commands when paired with an Amazon Echo. I wanted to experiment with Amazon Web Service (AWS) Internet of Things (IoT) services using my purchase of an Amazon Programmable Dash Button to control the TP-Link. The design point was that any click (single, double or long press) of the button would turn the power off if it’s currently powered on, and vice versa. That required querying the current device status, parsing the data returned and issuing the proper on/off command. Even the most simple projects, when dealing with unfamiliar technology, leads to lots of challenges and learning, which I’ll share in this blog.

I split the project in two parts, dealing first with controlling the TP-Link from a known environment, namely my MacBook Air on my home wireless network. There is no documented API for the TP-Link, but thanks to a Google search turning up a shell script on George Georgovassilis’s Techblog, I had a starting point. Commands are sent to TCP port 9999 on the TP-Link, which requires a statically-defined, internal IP address so it doesn’t move around, and I defined that on the wireless router. Executing the shell script from a Terminal prompt worked without issue. Knowing that I would be issuing those commands from the Internet (AWS) side of things and not having the ability to statically define the external IP address of my home’s Internet connection, I created a dynamic DNS name using the DYNU (www.dynu.com) service. Now when my home IP address changes, that gets sent to DYNU and they update DNS. I opened port 9999 inbound on my firewall to just the TP-Link, connected my Mac outside my home network via my Android phone’s hotspot capability. Testing was successful and the first part of the project was complete.

The second part dealt with understanding the parts and flows of AWS. When the IoT button is clicked, a message is sent to AWS which is mapped via a Simple Notification Service (SNS) message to one or more AWS Lambda functions. These functions can be written in Python 2.7 or Javascript (Node.js), but not the shell script I’d used so far. Deciding on Python, a language I had never used, I took a short crash course to learn its general syntax and converted the shell script, including updating the script’s netcat calls to standard Python socket calls. After several rounds of additional learning, I had a syntactically proper program and testing began. I was surprised to find that the data returned when querying the TP-Link was slightly different than before, and used a print statement to log the new string and modified the Python program to match. The final hurdle was learning that the socket needed to be closed and a new one created after the query and before the on/off command sent.

So now I have a working IoT button, but two factors limit its usefulness in this purpose. First, it takes about five seconds between clicking the button and the TP-Link changing its power status. Second, the IoT button is limited to about one thousand clicks before its power runs out with no way to charge or replace its battery. Turning lights on and off once a day would drain the button in a little over a year. Turning Christmas lights on and off once a day during each December would be a more suitable, and handy, use case.

Below is the code, with the only required change is updating DNS name (or IP address) in the two bolded connect statements,

AWS Lambda Python 2.7 Function

import socket
import base64
#
def lambda_handler(event, context):
  on = base64.b64decode(bytes('AAAAKtDygfiL/5r31e+UtsWg1Iv5nPCR6LfEsNGlwOLYo4HyhueT9tTu36Lfog=='))
#
  off = base64.b64decode(bytes('AAAAKtDygfiL/5r31e+UtsWg1Iv5nPCR6LfEsNGlwOLYo4HyhueT9tTu3qPeow=='))
#
  query = base64.b64decode(bytes('AAAAI9Dw0qHYq9+61/XPtJS20bTAn+yV5o/hh+jK8J7rh+vLtpbr'))
#
# Query the TP_Link for its current power status
  s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  s.connect(('YOUR IP OR DNS NAME', 9999))
  s.send(query)
  reply = base64.b64encode(bytes(s.recv(1024)))
  reply = reply[:7]
  s.close()
#
# If the TP_Link is off, turn it on, and vice versa
  s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  s.connect(('IP OR DNS NAME', 9999))
  if reply == 'AAACPND':
      s.send(on)
  else:
      s.send(off)
  s.close()

1 comment:

Mike B said...

Thanks for posting, I was able to get this working with guidance from this post