Page 1 of 1

Noob Help: Python script for TP-Link HS-110 Energy Monitor

PostPosted: Mon Mar 12, 2018 5:05 am
by sumocomputers
I do not know how to code at all, and tried reading some Indigo documentation, but realized I was in over my head. Here is what I want to do:

Run a Python script that returns the current power usage value from a TP-Link HS-110. The idea is to have it run every 1 or 2 seconds, so I can have near real-time usage displayed on a custom Indigo Control Page.

I am struggling on how to execute the script with the correct arguments, and how to set a variable from the return value that can be inserted in a custom control page (if that's even the way to do it).

The Python script when run from the command line on Mac looks something like this:
Code: Select all
python /Users/myUserName/Downloads/tplink-smartplug-master/tplink-smartplug.py -t 192.168.2.145 -j '{"emeter":{"get_realtime":{}}}'


And returns something like this (I am interested in the "power" value, rounded to nearest whole number):
Code: Select all
Sent:      {"emeter":{"get_realtime":{}}}
Received:  {"emeter":{"get_realtime":{"current":3.230671,"voltage":119.781098,"power":381.556378,"total":35.272000,"err_code":0}}}


Here is the Python Script (tplink-smartplug.py from https://github.com/softScheck/tplink-smartplug)
Code: Select all
#!/usr/bin/env python
#
# TP-Link Wi-Fi Smart Plug Protocol Client
# For use with TP-Link HS-100 or HS-110

# by Lubomir Stroetmann
# Copyright 2016 softScheck GmbH
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
#
import socket
import argparse

version = 0.1

# Check if IP is valid
def validIP(ip):
   try:
      socket.inet_pton(socket.AF_INET, ip)
   except socket.error:
      parser.error("Invalid IP Address.")
   return ip

# Predefined Smart Plug Commands
# For a full list of commands, consult tplink_commands.txt
commands = {'info'     : '{"system":{"get_sysinfo":{}}}',
         'on'       : '{"system":{"set_relay_state":{"state":1}}}',
         'off'      : '{"system":{"set_relay_state":{"state":0}}}',
         'cloudinfo': '{"cnCloud":{"get_info":{}}}',
         'wlanscan' : '{"netif":{"get_scaninfo":{"refresh":0}}}',
         'time'     : '{"time":{"get_time":{}}}',
         'schedule' : '{"schedule":{"get_rules":{}}}',
         'countdown': '{"count_down":{"get_rules":{}}}',
         'antitheft': '{"anti_theft":{"get_rules":{}}}',
         'reboot'   : '{"system":{"reboot":{"delay":1}}}',
         'reset'    : '{"system":{"reset":{"delay":1}}}'
}

# Encryption and Decryption of TP-Link Smart Home Protocol
# XOR Autokey Cipher with starting key = 171
def encrypt(string):
   key = 171
   result = "\0\0\0\0"
   for i in string:
      a = key ^ ord(i)
      key = a
      result += chr(a)
   return result

def decrypt(string):
   key = 171
   result = ""
   for i in string:
      a = key ^ ord(i)
      key = ord(i)
      result += chr(a)
   return result

# Parse commandline arguments
parser = argparse.ArgumentParser(description="TP-Link Wi-Fi Smart Plug Client v" + str(version))
parser.add_argument("-t", "--target", metavar="<ip>", required=True, help="Target IP Address", type=validIP)
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("-c", "--command", metavar="<command>", help="Preset command to send. Choices are: "+", ".join(commands), choices=commands)
group.add_argument("-j", "--json", metavar="<JSON string>", help="Full JSON string of command to send")
args = parser.parse_args()

# Set target IP, port and command to send
ip = args.target
port = 9999
if args.command is None:
   cmd = args.json
else:
   cmd = commands[args.command]



# Send command and receive reply
try:
   sock_tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
   sock_tcp.connect((ip, port))
   sock_tcp.send(encrypt(cmd))
   data = sock_tcp.recv(2048)
   sock_tcp.close()
   
   print "Sent:     ", cmd
   print "Received: ", decrypt(data[4:])
except socket.error:
   quit("Cound not connect to host " + ip + ":" + str(port))


Thanks,

Chris

Re: Noob Help: Python script for TP-Link HS-110 Energy Monit

PostPosted: Mon Mar 12, 2018 5:35 am
by howartp
That’s easily doable, and I suspect FlyingDiver will be along shortly as he’s answered a very similar question yesterday.

However with a little bit of research, I suspect that you can use GhostXML plugin to access the JSON directly from the IP address without needing a python script in between.


Sent from my iPhone using Tapatalk Pro

Re: Noob Help: Python script for TP-Link HS-110 Energy Monit

PostPosted: Mon Mar 12, 2018 8:29 am
by Colorado4Wheeler
This is untested, but if your Py script is pretty much functional as-is then a slight modification at the end would be all that you might need:

Code: Select all
#!/usr/bin/env python
#
# TP-Link Wi-Fi Smart Plug Protocol Client
# For use with TP-Link HS-100 or HS-110

# by Lubomir Stroetmann
# Copyright 2016 softScheck GmbH
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
#
import socket
import argparse

version = 0.1

# Check if IP is valid
def validIP(ip):
   try:
      socket.inet_pton(socket.AF_INET, ip)
   except socket.error:
      parser.error("Invalid IP Address.")
   return ip

# Predefined Smart Plug Commands
# For a full list of commands, consult tplink_commands.txt
commands = {'info'     : '{"system":{"get_sysinfo":{}}}',
         'on'       : '{"system":{"set_relay_state":{"state":1}}}',
         'off'      : '{"system":{"set_relay_state":{"state":0}}}',
         'cloudinfo': '{"cnCloud":{"get_info":{}}}',
         'wlanscan' : '{"netif":{"get_scaninfo":{"refresh":0}}}',
         'time'     : '{"time":{"get_time":{}}}',
         'schedule' : '{"schedule":{"get_rules":{}}}',
         'countdown': '{"count_down":{"get_rules":{}}}',
         'antitheft': '{"anti_theft":{"get_rules":{}}}',
         'reboot'   : '{"system":{"reboot":{"delay":1}}}',
         'reset'    : '{"system":{"reset":{"delay":1}}}'
}

# Encryption and Decryption of TP-Link Smart Home Protocol
# XOR Autokey Cipher with starting key = 171
def encrypt(string):
   key = 171
   result = "\0\0\0\0"
   for i in string:
      a = key ^ ord(i)
      key = a
      result += chr(a)
   return result

def decrypt(string):
   key = 171
   result = ""
   for i in string:
      a = key ^ ord(i)
      key = ord(i)
      result += chr(a)
   return result

# Parse commandline arguments
parser = argparse.ArgumentParser(description="TP-Link Wi-Fi Smart Plug Client v" + str(version))
parser.add_argument("-t", "--target", metavar="<ip>", required=True, help="Target IP Address", type=validIP)
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("-c", "--command", metavar="<command>", help="Preset command to send. Choices are: "+", ".join(commands), choices=commands)
group.add_argument("-j", "--json", metavar="<JSON string>", help="Full JSON string of command to send")
args = parser.parse_args()

# Set target IP, port and command to send
ip = args.target
port = 9999
if args.command is None:
   cmd = args.json
else:
   cmd = commands[args.command]



# Send command and receive reply
try:
   sock_tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
   sock_tcp.connect((ip, port))
   sock_tcp.send(encrypt(cmd))
   data = sock_tcp.recv(2048)
   sock_tcp.close()
   
   indigo.server.log("Sent:     ", cmd)
   var = indigo.variables[12345] # <<<<<<<< This would be the ID of the variable you want to put the value in
   var.value = unicode(decrypt(data[4:]))
   var.replaceOnServer()

except socket.error:
   indigo.server.log("Cound not connect to host " + ip + ":" + str(port))

Re: Noob Help: Python script for TP-Link HS-110 Energy Monit

PostPosted: Mon Mar 12, 2018 8:43 am
by FlyingDiver
Here's a stripped down version that just does what the OP wanted. Not tested.

Code: Select all
#!/usr/bin/env python
#
# TP-Link Wi-Fi Smart Plug Protocol Client
# For use with TP-Link HS-100 or HS-110

# by Lubomir Stroetmann
# Copyright 2016 softScheck GmbH
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
#
import socket

import indigo
import json

# Encryption and Decryption of TP-Link Smart Home Protocol
# XOR Autokey Cipher with starting key = 171
def encrypt(string):
   key = 171
   result = "\0\0\0\0"
   for i in string:
      a = key ^ ord(i)
      key = a
      result += chr(a)
   return result

def decrypt(string):
   key = 171
   result = ""
   for i in string:
      a = key ^ ord(i)
      key = ord(i)
      result += chr(a)
   return result


# Set target IP, port and command to send

ip = '192.168.2.145'
port = 9999
cmd = '{"emeter":{"get_realtime":{}}}'
var = indigo.variables[12345]           #  Change this to the actual ID of your variable

# Send command and receive reply
try:
    sock_tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock_tcp.connect((ip, port))
    sock_tcp.send(encrypt(cmd))
    data = sock_tcp.recv(2048)
    sock_tcp.close()
   
#   indigo.server.log("Sent:     ", cmd)
#   indigo.server.log("Received: ", decrypt(data[4:]))
   
    jsonData = json.loads(decrypt(data[4:]))
    var.value = unicode( jsonData["emeter"]["get_realtime"]["power"])
    var.replaceOnServer()

except socket.error:
    indigo.server.error("Cound not connect to host " + ip + ":" + str(port))

Re: Noob Help: Python script for TP-Link HS-110 Energy Monit

PostPosted: Mon Mar 12, 2018 8:45 am
by FlyingDiver
howartp wrote:
That’s easily doable, and I suspect FlyingDiver will be along shortly as he’s answered a very similar question yesterday.

However with a little bit of research, I suspect that you can use GhostXML plugin to access the JSON directly from the IP address without needing a python script in between.


I'm not that familiar with GhostXML, but I doubt there's a way to get it to do the device specific encrypt/decrypt needed here without a script.

Re: Noob Help: Python script for TP-Link HS-110 Energy Monit

PostPosted: Mon Mar 12, 2018 8:48 am
by sumocomputers
I am trying the modifications you provided by creating a Schedule > Server Actions > Execute Script (File) > Select the .py file

But having a problem. I normally specify the IP address at command line, and so it is returning errors:
Code: Select all
Script Error                    tplink.py: 'module' object has no attribute 'argv'
   Script Error                    Exception Traceback (most recent call shown last):

     tplink.py, line 71, at top level
     File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/argparse.py", line 1586, in __init__
       prog = _os.path.basename(_sys.argv[0])
AttributeError: 'module' object has no attribute 'argv'


So then I tried hard coding the IP address on link 79 replacing
Code: Select all
ip = args.target

With
Code: Select all
ip = 192.168.2.145


But then get the error:
Code: Select all
Script Error                    tplink.py: invalid syntax
   Script Error                    around line 79 - "ip = 192.168.2.145"

Re: Noob Help: Python script for TP-Link HS-110 Energy Monit

PostPosted: Mon Mar 12, 2018 8:49 am
by FlyingDiver
Use my script. I already fixed all that for you. ;)

Re: Noob Help: Python script for TP-Link HS-110 Energy Monit

PostPosted: Mon Mar 12, 2018 8:55 am
by sumocomputers
FlyingDiver wrote:
Use my script. I already fixed all that for you. ;)


Thank you for the help. I used your stripped down script and replaced the Variable ID. However, I am now getting this:

Code: Select all
Script Error                    tplink3.py: Python argument types in
    None.None(Variable, float)
did not match C++ signature:
    None(CVariableElem {lvalue}, CCString)
   Script Error                    Exception Traceback (most recent call shown last):

     tplink3.py, line 68, at top level
ArgumentError: Python argument types in
    None.None(Variable, float)
did not match C++ signature:
    None(CVariableElem {lvalue}, CCString)

Re: Noob Help: Python script for TP-Link HS-110 Energy Monit

PostPosted: Mon Mar 12, 2018 9:03 am
by FlyingDiver
Oops. I forgot that Indigo variables need to be strings. Fixed in script above.

Re: Noob Help: Python script for TP-Link HS-110 Energy Monit

PostPosted: Mon Mar 12, 2018 9:38 am
by sumocomputers
FlyingDiver wrote:
Oops. I forgot that Indigo variables need to be strings. Fixed in script above.


This is awesome, it works! Thank you so much!

Is there a way I can truncate or round up to a whole number either in Python script or Variable?

I was able to just visually cut off the decimal places in the control page, but that's kinda hokey :D

Re: Noob Help: Python script for TP-Link HS-110 Energy Monit

PostPosted: Mon Mar 12, 2018 9:44 am
by Colorado4Wheeler
Code: Select all
var.value = unicode(round(float( jsonData["emeter"]["get_realtime"]["power"]), 2))


that's for 2 points, for no points just:

Code: Select all
var.value = unicode(round(float( jsonData["emeter"]["get_realtime"]["power"]), 0))


or try

Code: Select all
var.value = unicode(int( jsonData["emeter"]["get_realtime"]["power"]))




If the value is a digit already. If not then unicode needs to be move inward.

Re: Noob Help: Python script for TP-Link HS-110 Energy Monit

PostPosted: Mon Mar 12, 2018 9:51 am
by sumocomputers
This is awesome, works great.

Now I am going off on my own and see if I can use your working code to make another script that can turn on/off the smart outlet. Please don't help me...yet.

I also think this is might be a great candidate for an Indigo Plugin, but baby steps. These are great, if not insecure, little devices, and would never buy a Kill-A-Watt again.

Thanks,

Chris