Python-KasaSmartPowerStrip icon indicating copy to clipboard operation
Python-KasaSmartPowerStrip copied to clipboard

Trying to port KasaSmartPowerStrip to CircuitPython using a Adafruit ESP32S3 Feather

Open jglogau opened this issue 2 years ago • 19 comments

I am trying to port the KSPS module to CircuitPython on a ESP32S3.

I have to switch the socket module to socketport, that seems to be a simple substitution but it stops there.

I am just cutting and pasting, very new to Python.

If this is a lot of work I might just stick to running the code on my Windows machine but would rather not.

Here is the Thonny Editor output, plus the changes I've made in the code:

%Run -c $EDITOR_CONTENT from KasaSmartPowerStrip import SmartPowerStrip power_strip = SmartPowerStrip('192.168.0.1') Traceback (most recent call last): File "", line 1, in File "KasaSmartPowerStrip.py", line 16, in init File "KasaSmartPowerStrip.py", line 42, in get_system_info File "KasaSmartPowerStrip.py", line 202, in _udp_send_command AttributeError: 'module' object has no attribute 'socket'

import socketpool import json import struct from builtins import bytes

class SmartPowerStrip(object):

def __init__(self, ip, device_id=None, timeout=2.0, protocol='tcp'):
    self.ip = ip
    self.port = 9999
    self.protocol = protocol
    self.device_id = device_id
    self.sys_info = None
    self.timeout = timeout

    self.sys_info = self.get_system_info()['system']['get_sysinfo']

    if not self.device_id:
        self.device_id = self.sys_info['deviceId']

def set_wifi_credentials(self, ssid, psk, key_type='3'):
    '''
    :param ssid: router ssid
    :param psk: router passkey
    :param key_type: 3 is WPA2, 2 might be WPA and 1 might be WEP?
    :return: command response
    '''

    wifi_command = '{"netif":{"set_stainfo":{"ssid":"' + ssid + '","password":"' + \
                   psk + '","key_type":' + key_type + '}}}'

    return self.send_command(wifi_command, self.protocol)

def set_cloud_server_url(self, server_url=''):

    server_command = '{"cnCloud":{"set_server_url":{"server":"' + server_url + '"}}}'

    return self.send_command(server_command, self.protocol)

def get_system_info(self):

    return self._udp_send_command('{"system":{"get_sysinfo":{}}}')

def get_realtime_energy_info(self, plug_num=None, plug_name=None):

    plug_id = self._get_plug_id(plug_num=plug_num, plug_name=plug_name)

    energy_command = '{"context":{"child_ids":["' + plug_id + '"]},"emeter":{"get_realtime":{}}}'

    response = self.send_command(energy_command, self.protocol)

    realtime_energy_data = response['emeter']['get_realtime']

    return realtime_energy_data

def get_historical_energy_info(self, month, year, plug_num=None, plug_name=None):

    plug_id = self._get_plug_id(plug_num=plug_num, plug_name=plug_name)

    energy_command = '{"context":{"child_ids":["' + plug_id + '"]},' + \
                     '"emeter":{"get_daystat":{"month": ' + month + ',"year":' + year + '}}}'

    response = self.send_command(energy_command, self.protocol)

    historical_energy_data = response['emeter']['get_daystat']['day_list']

    return historical_energy_data

def toggle_relay_leds(self, state):

    state_int = self._get_plug_state_int(state, reverse=True)

    led_command = '{"system":{"set_led_off":{"off":' + str(state_int) + '}}}'

    return self.send_command(led_command, self.protocol)

def set_plug_name(self, plug_num, plug_name):

    plug_id = self._get_plug_id(plug_num=plug_num)

    set_name_command = '{"context":{"child_ids":["' + plug_id + \
                       '"]},"system":{"set_dev_alias":{"alias":"' + plug_name + '"}}}'

    return self.send_command(set_name_command, self.protocol)

def get_plug_info(self, plug_num):

    target_plug = [plug for plug in self.sys_info['children'] if plug['id'] == str(int(plug_num)-1).zfill(2)]

    return target_plug

# toggle multiple plugs by id or name
def toggle_plugs(self, state, plug_num_list=None, plug_name_list=None):

    state_int = self._get_plug_state_int(state)

    plug_id_list_str = self._get_plug_id_list_str(plug_num_list=plug_num_list, plug_name_list=plug_name_list)

    all_relay_command = '{"context":{"child_ids":' + plug_id_list_str + '},' + \
                        '"system":{"set_relay_state":{"state":' + str(state_int) + '}}}'

    return self.send_command(all_relay_command, self.protocol)

# toggle a single plug
def toggle_plug(self, state, plug_num=None, plug_name=None):

    state_int = self._get_plug_state_int(state)

    plug_id = self._get_plug_id(plug_num=plug_num, plug_name=plug_name)

    relay_command = '{"context":{"child_ids":["' + plug_id + '"]},' + \
                '"system":{"set_relay_state":{"state":' + str(state_int) + '}}}'

    return self.send_command(relay_command, self.protocol)

def reboot(self, delay=1):
    reboot_command = '{"system":{"reboot":{"delay":' + str(delay) + '}}}'
    return self.send_command(reboot_command, self.protocol)

# manually send a command
def send_command(self, command, protocol='tcp'):

    if protocol == 'tcp':
        return self._tcp_send_command(command)
    elif protocol == 'udp':
        return self._udp_send_command(command)
    else:
        raise ValueError("Protocol must be 'tcp' or 'udp'")

def _get_plug_state_int(self, state, reverse=False):

    if state.lower() == 'on':
        if reverse:
            state_int = 0
        else:
            state_int = 1
    elif state.lower() == 'off':
        if reverse:
            state_int = 1
        else:
            state_int = 0
    else:
        raise ValueError("Invalid state, must be 'on' or 'off'")

    return state_int

# create a string with a list of plug_ids that can be inserted directly into a command
def _get_plug_id_list_str(self, plug_num_list=None, plug_name_list=None):

    plug_id_list = []

    if plug_num_list:
        for plug_num in plug_num_list:

            # add as str to remove the leading u
            plug_id_list.append(str(self._get_plug_id(plug_num=plug_num)))

    elif plug_name_list:

        for plug_name in plug_name_list:
            # add as str to remove the leading u
            plug_id_list.append(str(self._get_plug_id(plug_name=plug_name)))

    # convert to double quotes and turn the whole list into a string
    plug_id_list_str = str(plug_id_list).replace("'", '"')

    return plug_id_list_str

# get the plug child_id to be used with commands
def _get_plug_id(self, plug_num=None, plug_name=None):

    if plug_num and self.device_id:
        plug_id = self.device_id + str(plug_num-1).zfill(2)

    elif plug_name and self.sys_info:
        target_plug = [plug for plug in self.sys_info['children'] if plug['alias'] == plug_name]
        if target_plug:
            plug_id = self.device_id + target_plug[0]['id']
        else:
            raise ValueError('Unable to find plug with name ' + plug_name)
    else:
        raise ValueError('Unable to find plug.  Provide a valid plug_num or plug_name')

    return plug_id

def _tcp_send_command(self, command):

    sock_tcp = socketpool.socket(socketpool.AF_INET, socketpool.SOCK_STREAM)
    sock_tcp.settimeout(self.timeout)
    sock_tcp.connect((self.ip, self.port))

    sock_tcp.send(self._encrypt_command(command))

    data = sock_tcp.recv(2048)
    sock_tcp.close()

    # the first 4 chars are the length of the command so can be excluded
    return json.loads(self._decrypt_command(data[4:]))

def _udp_send_command(self, command):

    client_socket = socketpool.socket(socket.AF_INET, socket.SOCK_DGRAM)
    client_socket.settimeout(self.timeout)

    addr = (self.ip, self.port)

    client_socket.sendto(self._encrypt_command(command, prepend_length=False), addr)

    data, server = client_socket.recvfrom(2048)

    return json.loads(self._decrypt_command(data))

@staticmethod
def _encrypt_command(string, prepend_length=True):

    key = 171
    result = b''

    # when sending get_sysinfo using udp the length of the command is not needed but
    #  with all other commands using tcp it is
    if prepend_length:
        result = struct.pack(">I", len(string))

    for i in bytes(string.encode('latin-1')):
        a = key ^ i
        key = a
        result += bytes([a])
    return result

@staticmethod
def _decrypt_command(string):

    key = 171
    result = b''
    for i in bytes(string):
        a = key ^ i
        key = i
        result += bytes([a])
    return result.decode('latin-1')

jglogau avatar Jan 31 '23 00:01 jglogau

I am not able to test this myself but try starting over and just replace import socket with from socketpool import SocketPool as socket and that might work without any other changes.

p-doyle avatar Jan 31 '23 15:01 p-doyle

Hello:

Thank you for your reply, this is what I got:

I hope I put in the right command as you described:

from socketpool import SocketPool.socket as socket import json import struct from builtins import bytes

class SmartPowerStrip(object):

def __init__(self, ip, device_id=None, timeout=2.0, protocol='tcp'):
    self.ip = ip
    self.port = 9999
    self.protocol = protocol
    self.device_id = device_id
    self.sys_info = None
    self.timeout = timeout

    self.sys_info = self.get_system_info()['system']['get_sysinfo']

    if not self.device_id:
        self.device_id = self.sys_info['deviceId']

etc.

But there's an immediate error:

%Run -c $EDITOR_CONTENT

Traceback (most recent call last): File "", line 1 SyntaxError: invalid syntax

Thank you again for your help

Regards, Jordan Glogau

On Tue, Jan 31, 2023 at 10:32 AM p-doyle @.***> wrote:

I am not able to test this myself but try starting over and just replace import socket with from socketpool import SocketPool.socket as socket and that might work without any other changes.

— Reply to this email directly, view it on GitHub https://github.com/p-doyle/Python-KasaSmartPowerStrip/issues/10#issuecomment-1410593539, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABCMFG5RK2Y4PKLD43AP4SLWVEWAVANCNFSM6AAAAAAULZY3YU . You are receiving this because you authored the thread.Message ID: @.***>

jglogau avatar Jan 31 '23 18:01 jglogau

I just found this:

https://learn.adafruit.com/arduino-to-circuitpython/modules-and-importing

CircuitPython does not have the capability to import large modules available for full Python/CPython as the memory on microcontrollers is very limited.

Adafruit has committed to making a large number of modules available to support a broad range of hardware. This includes sensors, displays, smart LEDs (NeoPixel, etc.) and much more. Just as Adafruit is a leader in providing Open Source Arduino libraries, Adafruit is striving to do the same in the Python world.

There are more features to Python Modules but the above covers the basics. See a Python reference such as this one https://www.w3schools.com/python/default.asp to learn more.

Refer to the Adafruit GitHub repository https://github.com/adafruit for the latest CircuitPython modules available.

On Tue, Jan 31, 2023 at 1:27 PM Jordan Glogau @.***> wrote:

Hello:

Thank you for your reply, this is what I got:

I hope I put in the right command as you described:

from socketpool import SocketPool.socket as socket import json import struct from builtins import bytes

class SmartPowerStrip(object):

def __init__(self, ip, device_id=None, timeout=2.0, protocol='tcp'):
    self.ip = ip
    self.port = 9999
    self.protocol = protocol
    self.device_id = device_id
    self.sys_info = None
    self.timeout = timeout

    self.sys_info = self.get_system_info()['system']['get_sysinfo']

    if not self.device_id:
        self.device_id = self.sys_info['deviceId']

etc.

But there's an immediate error:

%Run -c $EDITOR_CONTENT

Traceback (most recent call last): File "", line 1 SyntaxError: invalid syntax

Thank you again for your help

Regards, Jordan Glogau

On Tue, Jan 31, 2023 at 10:32 AM p-doyle @.***> wrote:

I am not able to test this myself but try starting over and just replace import socket with from socketpool import SocketPool.socket as socket and that might work without any other changes.

— Reply to this email directly, view it on GitHub https://github.com/p-doyle/Python-KasaSmartPowerStrip/issues/10#issuecomment-1410593539, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABCMFG5RK2Y4PKLD43AP4SLWVEWAVANCNFSM6AAAAAAULZY3YU . You are receiving this because you authored the thread.Message ID: @.***>

jglogau avatar Jan 31 '23 18:01 jglogau

I edited the import statement shortly after I posted, can you try using the updated version? from socketpool import SocketPool as socket

p-doyle avatar Jan 31 '23 21:01 p-doyle

p-doyle:

Changed the first line in the code to:

from socketpool import SocketPool as socket

Got the same error

]0;🐍Wi-Fi: off | REPL | 8.0.0-beta.6 \

Adafruit CircuitPython 8.0.0-beta.6 on 2022-12-21; Adafruit Feather ESP32-S3 TFT with ESP32S3

from KasaSmartPowerStrip import SmartPowerStrip power_strip = SmartPowerStrip('192.168.0.1') Traceback (most recent call last): File "", line 1, in File "KasaSmartPowerStrip.py", line 16, in init File "KasaSmartPowerStrip.py", line 42, in get_system_info File "KasaSmartPowerStrip.py", line 202, in _udp_send_command AttributeError: 'module' object has no attribute 'socket'

BTY, just as an experiment I completely removed the import socket statement and got the same error.

Regards, Jordan Glogau

On Tue, Jan 31, 2023 at 4:45 PM p-doyle @.***> wrote:

I edited the import statement shortly after I posted, can you try using the updated version? from socketpool import SocketPool as socket

— Reply to this email directly, view it on GitHub https://github.com/p-doyle/Python-KasaSmartPowerStrip/issues/10#issuecomment-1411110639, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABCMFG7J2J6GLTRQGKFHTIDWVGBWTANCNFSM6AAAAAAULZY3YU . You are receiving this because you authored the thread.Message ID: @.***>

jglogau avatar Feb 01 '23 02:02 jglogau

Would you like me to send you the same Adafruit Feather ESP32-S3 TFT with ESP32S3 to play with,

Free of charge? These little guys are fun.

On Tue, Jan 31, 2023 at 9:10 PM Jordan Glogau @.***> wrote:

p-doyle:

Changed the first line in the code to:

from socketpool import SocketPool as socket

Got the same error

]0;🐍Wi-Fi: off | REPL | 8.0.0-beta.6 \

Adafruit CircuitPython 8.0.0-beta.6 on 2022-12-21; Adafruit Feather ESP32-S3 TFT with ESP32S3

from KasaSmartPowerStrip import SmartPowerStrip power_strip = SmartPowerStrip('192.168.0.1') Traceback (most recent call last): File "", line 1, in File "KasaSmartPowerStrip.py", line 16, in init File "KasaSmartPowerStrip.py", line 42, in get_system_info File "KasaSmartPowerStrip.py", line 202, in _udp_send_command AttributeError: 'module' object has no attribute 'socket'

BTY, just as an experiment I completely removed the import socket statement and got the same error.

Regards, Jordan Glogau

On Tue, Jan 31, 2023 at 4:45 PM p-doyle @.***> wrote:

I edited the import statement shortly after I posted, can you try using the updated version? from socketpool import SocketPool as socket

— Reply to this email directly, view it on GitHub https://github.com/p-doyle/Python-KasaSmartPowerStrip/issues/10#issuecomment-1411110639, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABCMFG7J2J6GLTRQGKFHTIDWVGBWTANCNFSM6AAAAAAULZY3YU . You are receiving this because you authored the thread.Message ID: @.***>

jglogau avatar Feb 01 '23 02:02 jglogau

P-Doyle:

Could I pay a small fee to get the port to Circuit Python done on the ESP32S3?

Would Paypal work for you?

Regards, Jordan Gloagu

On Tue, Jan 31, 2023 at 9:13 PM Jordan Glogau @.***> wrote:

Would you like me to send you the same Adafruit Feather ESP32-S3 TFT with ESP32S3 to play with,

Free of charge? These little guys are fun.

On Tue, Jan 31, 2023 at 9:10 PM Jordan Glogau @.***> wrote:

p-doyle:

Changed the first line in the code to:

from socketpool import SocketPool as socket

Got the same error

]0;🐍Wi-Fi: off | REPL | 8.0.0-beta.6 \

Adafruit CircuitPython 8.0.0-beta.6 on 2022-12-21; Adafruit Feather ESP32-S3 TFT with ESP32S3

from KasaSmartPowerStrip import SmartPowerStrip power_strip = SmartPowerStrip('192.168.0.1') Traceback (most recent call last): File "", line 1, in File "KasaSmartPowerStrip.py", line 16, in init File "KasaSmartPowerStrip.py", line 42, in get_system_info File "KasaSmartPowerStrip.py", line 202, in _udp_send_command AttributeError: 'module' object has no attribute 'socket'

BTY, just as an experiment I completely removed the import socket statement and got the same error.

Regards, Jordan Glogau

On Tue, Jan 31, 2023 at 4:45 PM p-doyle @.***> wrote:

I edited the import statement shortly after I posted, can you try using the updated version? from socketpool import SocketPool as socket

— Reply to this email directly, view it on GitHub https://github.com/p-doyle/Python-KasaSmartPowerStrip/issues/10#issuecomment-1411110639, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABCMFG7J2J6GLTRQGKFHTIDWVGBWTANCNFSM6AAAAAAULZY3YU . You are receiving this because you authored the thread.Message ID: @.***>

jglogau avatar Feb 02 '23 18:02 jglogau

No need for payment. I have one of the boards and will take a look this weekend.

p-doyle avatar Feb 02 '23 20:02 p-doyle

p-doyle:

Thank you very much, really appreciated.

Regards, Jordan Glogau

jglogau avatar Feb 02 '23 21:02 jglogau

I created a new branch with the changes, please see https://github.com/p-doyle/Python-KasaSmartPowerStrip/blob/CircuitPython/KasaSmartPowerStrip.py and let me know if that works.

p-doyle avatar Feb 07 '23 14:02 p-doyle

Hi:

I still think a small fee might help things move forward ;-)

If you don't have the bandwidth I may change to a 4 Channel Relay Board with or without WiFI/MQTT.

Regards, Jordan Glogau

On Thu, Feb 2, 2023 at 3:04 PM p-doyle @.***> wrote:

No need for payment. I have one of the boards and will take a look this weekend.

— Reply to this email directly, view it on GitHub https://github.com/p-doyle/Python-KasaSmartPowerStrip/issues/10#issuecomment-1414301325, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABCMFG3DU5KSR5NHXVGK2JTWVQHMRANCNFSM6AAAAAAULZY3YU . You are receiving this because you authored the thread.Message ID: @.***>

jglogau avatar Feb 14 '23 22:02 jglogau

Did you see my last reply? I've uploaded to changes to https://github.com/p-doyle/Python-KasaSmartPowerStrip/blob/CircuitPython/KasaSmartPowerStrip.py

p-doyle avatar Feb 15 '23 13:02 p-doyle

P-Doyle:

I missed that, thank you, I'll try it out after I get a new Feather. The last one seems to have bricked.

Regards, Jordan Glogau

jglogau avatar Feb 15 '23 16:02 jglogau

I tried the regular Python code and got this:

from KasaSmartPowerStrip import SmartPowerStrip power_strip = SmartPowerStrip('192.168.0.1') Traceback (most recent call last): File "", line 1, in File "C:\Users\jglog\Documents\Arduino\python\Python-KasaSmartPowerStrip-master\Python-KasaSmartPowerStrip-master\KasaSmartPowerStrip.py", line 16, in init self.sys_info = self.get_system_info()['system']['get_sysinfo'] File "C:\Users\jglog\Documents\Arduino\python\Python-KasaSmartPowerStrip-master\Python-KasaSmartPowerStrip-master\KasaSmartPowerStrip.py", line 42, in get_system_info return self._udp_send_command('{"system":{"get_sysinfo":{}}}') File "C:\Users\jglog\Documents\Arduino\python\Python-KasaSmartPowerStrip-master\Python-KasaSmartPowerStrip-master\KasaSmartPowerStrip.py", line 209, in _udp_send_command data, server = client_socket.recvfrom(2048) TimeoutError: timed out

I would imagine I am doing something wrong but can't tell. I tried the old version and get the same error, so I am not at all sure what I configured differently

jglogau avatar Feb 15 '23 20:02 jglogau

You are running that script on the feather, correct? Are you connecting to the power strip's wifi network first?

p-doyle avatar Feb 15 '23 20:02 p-doyle

This is the error back, doesn't like the syntax

Traceback (most recent call last): File "C:\Users\jglog\Documents\Arduino\python\Python-KasaSmartPowerStrip-master-old\KasaSmartPowerStrip.py", line 1 from socketpool import SocketPool.socket as socket ^ SyntaxError: invalid syntax

On Wed, Feb 15, 2023 at 8:47 AM p-doyle @.***> wrote:

Did you see my last reply? I've uploaded to changes to https://github.com/p-doyle/Python-KasaSmartPowerStrip/blob/CircuitPython/KasaSmartPowerStrip.py

— Reply to this email directly, view it on GitHub https://github.com/p-doyle/Python-KasaSmartPowerStrip/issues/10#issuecomment-1431397115, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABCMFG755SNM57W4IVT3X5TWXTM77ANCNFSM6AAAAAAULZY3YU . You are receiving this because you authored the thread.Message ID: @.***>

jglogau avatar Feb 15 '23 21:02 jglogau

I am running the revised regular script on Windows 10.

As I said in my previous email, my Feather seems to have bricked. Waiting for an answer from Adafruit in that regard.

So I was just experimenting with the regular revised version and that is where the problem came from.

Plus the SyntaxError itself seems wrong, but what do I know?

error is from.

On Wed, Feb 15, 2023 at 4:06 PM Jordan Glogau @.***> wrote:

This is the error back, doesn't like the syntax

Traceback (most recent call last): File "C:\Users\jglog\Documents\Arduino\python\Python-KasaSmartPowerStrip-master-old\KasaSmartPowerStrip.py", line 1 from socketpool import SocketPool.socket as socket ^ SyntaxError: invalid syntax

On Wed, Feb 15, 2023 at 8:47 AM p-doyle @.***> wrote:

Did you see my last reply? I've uploaded to changes to https://github.com/p-doyle/Python-KasaSmartPowerStrip/blob/CircuitPython/KasaSmartPowerStrip.py

— Reply to this email directly, view it on GitHub https://github.com/p-doyle/Python-KasaSmartPowerStrip/issues/10#issuecomment-1431397115, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABCMFG755SNM57W4IVT3X5TWXTM77ANCNFSM6AAAAAAULZY3YU . You are receiving this because you authored the thread.Message ID: @.***>

jglogau avatar Feb 15 '23 21:02 jglogau

If you are running this on Windows please make sure to use the original script at https://github.com/p-doyle/Python-KasaSmartPowerStrip/blob/master/KasaSmartPowerStrip.py

p-doyle avatar Feb 17 '23 15:02 p-doyle

Sorry for the late reply.

I got the regular script for Python3 on windows working without a problem. Connected to the wrong wifi without realizing it.

Here the latest error from the CircuitPython version:

%Run -c $EDITOR_CONTENT

  • from* KasaSmartPowerStrip import SmartPowerStrip power_strip = SmartPowerStrip('192.168.0.1')

Traceback (most recent call last): File "", line 1, in

File "KasaSmartPowerStrip.py", line 16, in init File "KasaSmartPowerStrip.py", line 42, in get_system_info File "KasaSmartPowerStrip.py", line 202, in _udp_send_command AttributeError: 'module' object has no attribute 'socket'

On Fri, Feb 17, 2023 at 10:44 AM p-doyle @.***> wrote:

If you are running this on Windows please make sure to use the original script at https://github.com/p-doyle/Python-KasaSmartPowerStrip/blob/master/KasaSmartPowerStrip.py

— Reply to this email directly, view it on GitHub https://github.com/p-doyle/Python-KasaSmartPowerStrip/issues/10#issuecomment-1434826279, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABCMFG2R4FAQNHTZYNQLQY3WX6MFHANCNFSM6AAAAAAULZY3YU . You are receiving this because you authored the thread.Message ID: @.***>

jglogau avatar Feb 28 '23 09:02 jglogau