TP-Link smart plug HS100/HS110

Others (MiLight, Hue, Toon etc...)
cdk222
Posts: 3
Joined: Thursday 01 March 2018 15:55
Target OS: Raspberry Pi
Domoticz version:
Contact:

Re: TP-Link smart plug HS100/HS110

Post by cdk222 » Thursday 01 March 2018 16:11

Hi Mike

I am receiving similar errors to MnM001 and have been spinning my wheels on it for a couple of days now:

Traceback (most recent call last):
File "./tplink-smartplug-HS110.py", line 60, in <module>
jsonData = json.loads(result)
File "/usr/lib/python2.7/json/__init__.py", line 339, in loads
return _default_decoder.decode(s)
File "/usr/lib/python2.7/json/decoder.py", line 364, in decode
obj, end = self.raw_decode(s, idx=_w(s, 0).end())
File "/usr/lib/python2.7/json/decoder.py", line 382, in raw_decode
raise ValueError("No JSON object could be decoded")
ValueError: No JSON object could be decoded

Code: Select all

#!/usr/bin/env python
# 
# TP-Link Wi-Fi Smart Plug Protocol Client
# For use with TP-Link HS110: energy monitor
# 
# Gets current power (W) and cumulative energy (kWh)
# and sends to Domoticz

import socket
import argparse
import json
import urllib
import urllib2

# ip, port, and command for HS110
ip = '10.1.1.5'
port = 9999
cmd = '{"emeter":{"get_realtime":{}}}'

# Domoticz command stub and IDx of HS110
baseURL = 'http://<10.1.1.5:8080>/json.htm?type=command&param=udevice&nvalue=0'
HSIdx = 2

# 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

def domoticzrequest (url):
   request = urllib2.Request(url)
#   request.add_header("Authorization", "Basic %s" % base64string)
   response = urllib2.urlopen(request)
   return None;

# 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
	result = decrypt(data[4:])
	jsonData = json.loads(result)
#	print "Received: "
#	print json.dumps(jsonData, indent=4, sort_keys=True)
	power = jsonData['emeter']['get_realtime']['power']
	total = jsonData['emeter']['get_realtime']['total'] * 1000
#	print power, total
except socket.error:
	quit("Cound not connect to host " + ip + ":" + str(port))

# Send data to Domoticz
try:
    url = baseURL + "&idx=%s&svalue=%s;%s" % (HSIdx, power, total)
    domoticzrequest(url)
except urllib2.URLError, e:
	print e.code
	
Looking at https://www.domoticz.com/wiki/Domoticz_API/JSON_URL%27s i did notice a slight variation in the syntax they use for the following:

Electricity (instant and counter)
/json.htm?type=command&param=udevice&idx=IDX&nvalue=0&svalue=POWER;ENERGY

........however others including yourself have this working with your code so im not sure where I am going wrong. I have messed around with the IP & port but no luck. I can turn the plug on\off using the another HSxxx script (tplink-smartplug.py).

Please Help!!!

MikeF
Posts: 168
Joined: Sunday 19 April 2015 0:36
Target OS: Raspberry Pi
Domoticz version: V3.8153
Location: UK
Contact:

Re: TP-Link smart plug HS100/HS110

Post by MikeF » Thursday 01 March 2018 18:32

@cdk222,

First of all, make sure that ip = '10.1.1.5' is the ip address of your HS110; you've got the same ip address in baseURL - this should be your Domoticz ip. Also, remove the <> here, and try again. Then see below, if this doesn't work.

@MnM001,

I've come across a bash script for HS100 / HS110 here: https://github.com/ggeorgovassilis/linuxscripts.
On my HS110 I get:

Code: Select all

./HS100.sh 192.168.0.84 9999 on
found 0 associations
found 1 connections:
     1:	flags=82<CONNECTED,PREFERRED>
	outif en0
	src 192.168.0.10 port 63787
	dst 192.168.0.84 port 9999
	rank info not available
	TCP aux info available

Connection to 192.168.0.84 port 9999 [tcp/distinct] succeeded!
This bash script has some interesting dependencies and diagnostics, which might have a bearing.
If this (and your 'python tplink-smartplug.py -t 192.168.0.151 -c time') doesn't work, then I'm wondering if TP-Link have changed the encryption / decryption of these devices?
If so, I don't know what to suggest (I was following this website: https://www.softscheck.com/en/reverse-e ... ink-hs110/).

cdk222
Posts: 3
Joined: Thursday 01 March 2018 15:55
Target OS: Raspberry Pi
Domoticz version:
Contact:

Re: TP-Link smart plug HS100/HS110

Post by cdk222 » Saturday 03 March 2018 11:36

Thanks Mike

I had tried numerous combinations of IP and unfortunately I left the incorrect one in the code I posted.

Have spend the day playing around with the code and found numerous updates that needed to be made for the newer version of Python. In particular the urllib commands and the HS110 'power' and 'total' syntax.

The below worked in the end for me although I need to update the value's sent to domoticz......next on the list

Thanks again

Code: Select all

#!/usr/bin/python3
#
#################################################################################################
#################################################################################################
#
# TP-Link Wi-Fi Smart Plug Protocol Energy Monitoring Client
# For use with TP-Link HS-110
#
#
# Additional Information:
#
#      https://www.domoticz.com/forum/viewtopic.php?f=56&t=13290
#      https://www.softscheck.com/en/reverse-engineering-tp-link-hs110/
#
# Complete URL String to Send to Domoticz
#      http://DOMSERVER_IP:8080/json.htm?type=command&param=udevice&nvalue=0&idx=IDX&svalue=POWER;TOTAL
#
#################################################################################################
#################################################################################################

import socket
import argparse
import json
import urllib
import urllib.request

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}}}',
                        'energy'    : '{"emeter":{"get_realtime":{}}}'
}

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


def decrypt(string):
    key = 171
    result = ""
    for i in string:
        a = key ^ i
        key = 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()

# Domoticz command stub and IDx of HS110
baseURL = 'http://<DOMOTICZ IP>:8080/json.htm?type=command&param=udevice&nvalue=0'
HSIdx = <IDX from Domoticz>

# 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()

        cmdresult = decrypt(data[4:])
        jsonData = json.loads(cmdresult)
        power = jsonData['emeter']['get_realtime']['power_mw']
        total = jsonData['emeter']['get_realtime']['total_wh'] * 1000
        url = baseURL + "&idx=%s&svalue=%s;%s" % (HSIdx, power, total)
        domreq = urllib.request.Request(url)

# Troubleshooting Print Commands
#       print (json.dumps(jsonData, indent=4,sort_keys=True))
#       print(power,total)
#       print(url)
#       print("Sent:     ", cmd)
#       print("Received: ", decrypt(data[4:]))

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

try:
        urllib.request.urlopen(domreq)
except Exception as e:
        print(str(e))

MikeF
Posts: 168
Joined: Sunday 19 April 2015 0:36
Target OS: Raspberry Pi
Domoticz version: V3.8153
Location: UK
Contact:

Re: TP-Link smart plug HS100/HS110

Post by MikeF » Saturday 03 March 2018 17:26

@cdk222,

Glad you seem to have got this working - maybe the difference in our experiences is because I am using Python 2.7?

cdk222
Posts: 3
Joined: Thursday 01 March 2018 15:55
Target OS: Raspberry Pi
Domoticz version:
Contact:

Re: TP-Link smart plug HS100/HS110

Post by cdk222 » Sunday 04 March 2018 6:09

@Mike

Thats exactly what it was. Thanks for the original code

cazz
Posts: 15
Joined: Sunday 04 March 2018 19:12
Target OS: Linux
Domoticz version:
Contact:

Re: TP-Link smart plug HS100/HS110

Post by cazz » Tuesday 06 March 2018 20:54

Just here to say thanks MikeF for the script that you post in page 1
Have a HS100 and was curious if I can use that too read the Watt for me.

MikeF
Posts: 168
Joined: Sunday 19 April 2015 0:36
Target OS: Raspberry Pi
Domoticz version: V3.8153
Location: UK
Contact:

Re: TP-Link smart plug HS100/HS110

Post by MikeF » Wednesday 07 March 2018 13:20

The HS100 is just a wifi smart plug - the HS110 includes energy monitoring.

cazz
Posts: 15
Joined: Sunday 04 March 2018 19:12
Target OS: Linux
Domoticz version:
Contact:

Re: TP-Link smart plug HS100/HS110

Post by cazz » Wednesday 07 March 2018 16:59

Sorry I mean HS110 :)

kawa
Posts: 1
Joined: Wednesday 14 March 2018 5:15
Target OS: Linux
Domoticz version:
Contact:

Re: TP-Link smart plug HS100/HS110

Post by kawa » Wednesday 14 March 2018 5:35

For those using python3 you can use the following python code:

To switch on/off (tplink-smartplug.py)

Code: Select all

#!/usr/bin/env python3
#
# 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 
# 
# modified by Kawa
#
# 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.5

# 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 = b"\0\0\0"+ chr(len(string)).encode('utf-8')
	for i in string.encode('utf-8'):
		a = key ^ i
		key = a
		result += chr(a).encode('utf-8')
	return result

def decrypt(string):
	key = 171 
	result = ""
	for i in string: 
		a = key ^ i
		key = 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))
Calling the script by adding the following on/off actions to the virtual switch (my tplink plug has ip 192.168.1.32):

Code: Select all

script://tplink-smartplug.py -t 192.168.1.32 -c on

Code: Select all

script://tplink-smartplug.py -t 192.168.1.32 -c off

I have the following script to get the status (tplink-status.py)

Code: Select all

#!/usr/bin/env python3
#
# TP-Link Wi-Fi Smart Plug Protocol Client
# For use with TP-Link HS100
#
# modified by Kawa
#
# Gets current switch status
# and sends to Domoticz

import socket
import argparse
import json
import urllib.request, urllib.parse, urllib.error
import urllib.request, urllib.error, urllib.parse


# 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 


parser = argparse.ArgumentParser(description="TP-Link Wi-Fi Smart Plug Client")
parser.add_argument("-t", "--target", metavar="<ip>", required=True, help="Target IP Address", type=validIP)
parser.add_argument("-i", "--idx", metavar="<idx>", help="Which Domoticz IDX")
parser.add_argument("-d", "--domoticz", metavar="<Domoticz IP>", help="Full IP address with port number for Domoticz")
args = parser.parse_args()


# ip, port, and command for HS100
ip = args.target
port = 9999
cmd = '{"system":{"get_sysinfo":{}}}'
idx = args.idx
domoticzIP = args.domoticz

# Domoticz command stub and IDx of HS100
baseURL = 'http://' + domoticzIP + '/json.htm?type=command&param=udevice&idx=' + str(idx) + '&nvalue='

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

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

def domoticzrequest (url):
   request = urllib.request.Request(url)
   response = urllib.request.urlopen(request)
   result = response.read()
   return result;

def getcurrentstate():
   url = 'http://' + domoticzIP + '/json.htm?type=devices&rid=' + str(idx)
   response = domoticzrequest(url)
   jsonData = json.loads(response)
   status = jsonData['result'][0]['Status']
   lampState = 0
   if status == "On":
      lampState = 1

   return lampState;

# 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()

   result = decrypt(data[4:])
   jsonData = json.loads(result)
   relay_state = jsonData['system']['get_sysinfo']['relay_state']
except socket.error:
   quit("Cound not connect to host " + ip + ":" + str(port))

# Send data to Domoticz
try:
   currentState = getcurrentstate()
   if currentState != relay_state:
      url = baseURL + str(relay_state)
      domoticzrequest(url)
      
except urllib.error.URLError as e:
   print((e.code))
I have the following line in crontab:
(the plug has ip 192.168.1.32 and the virtual switch has idx 5 on domoticz running on 192.168.1.50:8080

Code: Select all

*       *       *       *       *       /config/scripts/tplink-status.py -t 192.168.1.32 -i 5 -d 192.168.1.50:8080 > /var/log/tplink.log

Post Reply

Who is online

Users browsing this forum: No registered users and 1 guest