TP-Link HS110 Energy Monitoring API – addendum – Python3 script

Updating the Python2 script from softScheck to Python3, combining readings from 4 devices, and output to an I2C Oled display

Relating to my earlier post: https://blog.xarta.co.uk/2019/12/tp-link-hs110-energy-monitoring-api/

This code is a proof of concept for updating the OLED I2C display on my mains power-management box controlled by a Raspberry Pi 3B, pending my C# dotnet core 3 library. I’m not keen on Python, but if I must use Python I prefer Python 3 so I’ve updated the existing Python 2 code from softScheck to Python 3.

Important – this code doesn’t handle exceptions in a graceful way to continue running; there’s no reliable “watchdog” or timed interrupt / cron job or anything … this just loops round essentially until it gets interrupted or gets an exception (socket doesn’t respond in time – something glitches). It’s just to see how well the idea works.

...

#!/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
#
# 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.
#

# -------------------------------------------------------
# UPDATED TO PYTHON3 BY DAVE@XARTA.CO.UK DECEMBER 2019
# REMOVING COMMANDS LINES - ENERGY READING ONLY FOR LIST OF IPS
# ADDING JSON DESERIALISATION
# ADDING THREADING FOR SCHEDULING REPEAT RUNS
# ADDING I2C DISPLAY OUTPUT
# -------------------------------------------------------

import socket
import json
import threading
from struct import pack

# USING SOME CODE FROM ADAFRUIT SSD1306 EXAMPLES
import Adafruit_SSD1306

from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont

version = 0.3


# Raspberry Pi pin configuration:
RST = None     # on the PiOLED this pin isnt used

disp = Adafruit_SSD1306.SSD1306_128_32(rst=RST)

# Initialize library.
disp.begin()

# Clear display.
disp.clear()
disp.display()

# Create blank image for drawing.
# Make sure to create image with mode '1' for 1-bit color.
width = disp.width
height = disp.height
image = Image.new('1', (width, height))

# Get drawing object to draw on image.
draw = ImageDraw.Draw(image)

# Draw a black filled box to clear the image.
draw.rectangle((0,0,width,height), outline=0, fill=0)

# Draw some shapes.
# First define some constants to allow easy resizing of shapes.
padding = -2
top = padding
bottom = height-padding
# Move left to right keeping track of the current x position for drawing shapes.
x = 0

# found a fixed-width font I like at: https://www.dafont.com/theme.php?cat=503
font = ImageFont.truetype('whitrabt.ttf', 28)

# Check if hostname is valid
def validHostname(hostname):
    try:
        socket.gethostbyname(hostname)
    except socket.error:
        parser.error("Invalid hostname.")
    return hostname


# Encryption and Decryption of TP-Link Smart Home Protocol
# XOR Autokey Cipher with starting key = 171
# Dave modified for Python3
def encrypt(string):
    key = 171
    result = pack('>I', len(string))
    for i in string.encode("ascii"):
        a = key ^ i
        key = a
        result = result + bytes([a])
    return result

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




globalcurrent = 0
globalvoltage = 0
globalpower = 0
globalcounter = 0

port = 9999

# Send command and receive reply
def getreading(ip, cmd):
    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()

        received = decrypt(data[4:])

        if(len(data) > 0):
            global globalcounter
            globalcounter += 1

            energyreading = json.loads(received)
            
            if(received.find("current_ma") > 0):
                current = energyreading['emeter']['get_realtime']['current_ma'] / 1000
                voltage = energyreading['emeter']['get_realtime']['voltage_mv'] / 1000
                power   = energyreading['emeter']['get_realtime']['power_mw'] / 1000
            else:
                current = energyreading['emeter']['get_realtime']['current']
                voltage = energyreading['emeter']['get_realtime']['voltage']
                power   = energyreading['emeter']['get_realtime']['power']     

            global globalcurrent
            globalcurrent += current
            global globalvoltage
            globalvoltage += voltage
            global globalpower
            globalpower += power

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


def getreadings():
    threading.Timer(1.0, getreadings).start()
    global globalcurrent
    global globalvoltage
    global globalpower
    global globalcounter

    globalcurrent = 0
    globalvoltage = 0
    globalpower = 0
    globalcounter = 0

    for ip in ['192.168.13.31', '192.168.13.32', '192.168.13.40', '192.168.13.41']:
        getreading(ip, '{"emeter":{"get_realtime":{}}}')
    globalvoltage = globalvoltage / globalcounter

    #print(round(globalcurrent,2))
    #print(round(globalvoltage,2))
    #print(round(globalpower,2))

    # Draw a black filled box to clear the image.
    draw.rectangle((0,0,width,height), outline=0, fill=0)

    draw.text((x+8, top+8), '{:<03.2f}'.format(round(globalpower,2)) + "W", font=font, fill=255)

    # Display image.
    disp.image(image)
    disp.display()

getreadings()

Visual Studio Code with SSH connection to Pi for remote programming, showing htop running in the terminal
Keeping an eye on resource use. The Pi is running headless. I’ve set-up my two-factor TeamViewer account to include this which is useful for 4G failover – I don’t have to worry about serving anything over 4G or using a cloud provisioned VPN etc. Internally I connect via SSH and I’m using Visual Studio Code here over SSH although I haven’t set-up for remote debugging of Python yet – that’s just set-up for C#.
Even when I shut my “day” stuff down this is still the ball-mark electricity use for my computer set-up. Oh – and the Boiler which can use up to 100W with it’s ancient circuits (that’s wired from an extension from a TP-Link socket but still has an accessible 2-pole isolation switch if not a mains-ring connected fused spur). Obviously I’m uncomfortable with this energy guzzling but it’s driven by not affording more efficient and capable hardware and provision for redundancy and resilience to power issues etc. I want to move some of this power usage to a collocation if I can afford it when I’m working again: that will tackle some of my anxieties about probability of theft, power issues and fire from a data-asset protection and service provision POV – which in turn should allow me to look at more power-efficient deployment of systems at home. I would like to see this night-time power usage drop to < 100W for a minimum service back-up / failover and primarily home IoT equipment.