diff --git a/README.md b/README.md index 258d06e1..0232eee1 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,8 @@ If you are using cpython, it is recommended to install with `[speedups]` to enab pip install python-kasa[speedups] ``` +With `[speedups]`, the protocol overhead is roughly an order of magnitude lower (benchmarks available in devtools). + Alternatively, you can clone this repository and use poetry to install the development version: ``` git clone https://github.com/python-kasa/python-kasa.git diff --git a/devtools/README.md b/devtools/README.md index fc3ce883..92e91fd7 100644 --- a/devtools/README.md +++ b/devtools/README.md @@ -59,3 +59,13 @@ id -HS110(EU) 5.0 0.055700 0.016174 0.042086 0.045578 0.048905 0.059869 0.082064 -KP303(UK) 5.0 0.010298 0.003765 0.007773 0.007968 0.008546 0.010439 0.016763 ``` + +## benchmark + +* Benchmark the protocol + +```shell +% python3 devtools/bench/benchmark.py +New parser, parsing 100000 messages took 0.6339647499989951 seconds +Old parser, parsing 100000 messages took 9.473990250000497 seconds +``` diff --git a/devtools/bench/benchmark.py b/devtools/bench/benchmark.py new file mode 100644 index 00000000..2cdbd43e --- /dev/null +++ b/devtools/bench/benchmark.py @@ -0,0 +1,30 @@ +"""Benchmark the new parser against the old parser.""" + +import json +import timeit + +import orjson +from kasa_crypt import decrypt, encrypt +from utils.data import REQUEST, WIRE_RESPONSE +from utils.original import OriginalTPLinkSmartHomeProtocol + + +def original_request_response() -> None: + """Benchmark the original parser.""" + OriginalTPLinkSmartHomeProtocol.encrypt(json.dumps(REQUEST)) + json.loads(OriginalTPLinkSmartHomeProtocol.decrypt(WIRE_RESPONSE[4:])) + + +def new_request_response() -> None: + """Benchmark the new parser.""" + encrypt(orjson.dumps(REQUEST).decode()) + orjson.loads(decrypt(WIRE_RESPONSE[4:])) + + +count = 100000 + +time = timeit.Timer(new_request_response).timeit(count) +print(f"New parser, parsing {count} messages took {time} seconds") + +time = timeit.Timer(original_request_response).timeit(count) +print(f"Old parser, parsing {count} messages took {time} seconds") diff --git a/devtools/bench/utils/__init__.py b/devtools/bench/utils/__init__.py new file mode 100644 index 00000000..d49281d9 --- /dev/null +++ b/devtools/bench/utils/__init__.py @@ -0,0 +1 @@ +"""Benchmark utils.""" diff --git a/devtools/bench/utils/data.py b/devtools/bench/utils/data.py new file mode 100644 index 00000000..13a49e87 --- /dev/null +++ b/devtools/bench/utils/data.py @@ -0,0 +1,141 @@ +"""Test data for benchmarks.""" + + +import json + +from .original import OriginalTPLinkSmartHomeProtocol + +REQUEST = { + "system": {"get_sysinfo": None}, + "anti_theft": {"get_rules": None, "get_next_action": None}, + "schedule": { + "get_rules": None, + "get_next_action": None, + "get_realtime": None, + "get_daystat": {"year": 2023, "month": 6}, + "get_monthstat": {"year": 2023}, + }, + "time": {"get_time": None, "get_timezone": None}, + "emeter": { + "get_realtime": None, + "get_daystat": {"year": 2023, "month": 6}, + "get_monthstat": {"year": 2023}, + }, +} +RESPONSE = { + "anti_theft": { + "get_next_action": {"err_code": -2, "err_msg": "member not support"}, + "get_rules": {"enable": 0, "err_code": 0, "rule_list": [], "version": 2}, + }, + "emeter": { + "get_daystat": { + "day_list": [{"day": 30, "energy_wh": 0, "month": 6, "year": 2023}], + "err_code": 0, + }, + "get_monthstat": { + "err_code": 0, + "month_list": [{"energy_wh": 0, "month": 6, "year": 2023}], + }, + "get_realtime": { + "current_ma": 0, + "err_code": 0, + "power_mw": 0, + "slot_id": 0, + "total_wh": 0, + "voltage_mv": 119390, + }, + }, + "schedule": { + "get_daystat": { + "day_list": [{"day": 30, "month": 6, "time": 3, "year": 2023}], + "err_code": 0, + }, + "get_monthstat": { + "err_code": 0, + "month_list": [{"month": 6, "time": 3, "year": 2023}], + }, + "get_next_action": {"err_code": 0, "type": -1}, + "get_realtime": {"err_code": -2, "err_msg": "member not support"}, + "get_rules": {"enable": 1, "err_code": 0, "rule_list": [], "version": 2}, + }, + "system": { + "get_sysinfo": { + "alias": "TP-LINK_Power Strip_5C33", + "child_num": 6, + "children": [ + { + "alias": "Plug 1", + "id": "8006AF35494E7DB13DDE9B8F40BF2E001E77031900", + "next_action": {"type": -1}, + "on_time": 231, + "state": 1, + }, + { + "alias": "Plug 2", + "id": "8006AF35494E7DB13DDE9B8F40BF2E001E77031901", + "next_action": {"type": -1}, + "on_time": 231, + "state": 1, + }, + { + "alias": "Plug 3", + "id": "8006AF35494E7DB13DDE9B8F40BF2E001E77031902", + "next_action": {"type": -1}, + "on_time": 231, + "state": 1, + }, + { + "alias": "Plug 4", + "id": "8006AF35494E7DB13DDE9B8F40BF2E001E77031903", + "next_action": {"type": -1}, + "on_time": 231, + "state": 1, + }, + { + "alias": "Plug 5", + "id": "8006AF35494E7DB13DDE9B8F40BF2E001E77031904", + "next_action": {"type": -1}, + "on_time": 231, + "state": 1, + }, + { + "alias": "Plug 6", + "id": "8006AF35494E7DB13DDE9B8F40BF2E001E77031905", + "next_action": {"type": -1}, + "on_time": 231, + "state": 1, + }, + ], + "deviceId": "8006AF35494E7DB13DDE9B8F40BF2E001E770319", + "err_code": 0, + "feature": "TIM:ENE", + "hwId": "955F433CBA24823A248A59AA64571A73", + "hw_ver": "2.0", + "latitude_i": 297852, + "led_off": 0, + "longitude_i": -954074, + "mac": "C0:06:C3:42:5C:33", + "mic_type": "IOT.SMARTPLUGSWITCH", + "model": "HS300(US)", + "oemId": "32BD0B21AA9BF8E84737D1DB1C66E883", + "rssi": -41, + "status": "new", + "sw_ver": "1.0.3 Build 201203 Rel.165457", + "updating": 0, + } + }, + "time": { + "get_time": { + "err_code": 0, + "hour": 9, + "mday": 30, + "min": 32, + "month": 6, + "sec": 54, + "year": 2023, + }, + "get_timezone": {"err_code": 0, "index": 13}, + }, +} + +WIRE_RESPONSE = OriginalTPLinkSmartHomeProtocol.encrypt(json.dumps(RESPONSE)) diff --git a/devtools/bench/utils/original.py b/devtools/bench/utils/original.py new file mode 100644 index 00000000..67aeaa33 --- /dev/null +++ b/devtools/bench/utils/original.py @@ -0,0 +1,47 @@ +"""Original implementation of the TP-Link Smart Home protocol.""" +import struct +from typing import Generator + + +class OriginalTPLinkSmartHomeProtocol: + """Original implementation of the TP-Link Smart Home protocol.""" + + INITIALIZATION_VECTOR = 171 + + @staticmethod + def _xor_payload(unencrypted: bytes) -> Generator[int, None, None]: + key = OriginalTPLinkSmartHomeProtocol.INITIALIZATION_VECTOR + for unencryptedbyte in unencrypted: + key = key ^ unencryptedbyte + yield key + + @staticmethod + def encrypt(request: str) -> bytes: + """Encrypt a request for a TP-Link Smart Home Device. + + :param request: plaintext request data + :return: ciphertext to be send over wire, in bytes + """ + plainbytes = request.encode() + return struct.pack(">I", len(plainbytes)) + bytes( + OriginalTPLinkSmartHomeProtocol._xor_payload(plainbytes) + ) + + @staticmethod + def _xor_encrypted_payload(ciphertext: bytes) -> Generator[int, None, None]: + key = OriginalTPLinkSmartHomeProtocol.INITIALIZATION_VECTOR + for cipherbyte in ciphertext: + plainbyte = key ^ cipherbyte + key = cipherbyte + yield plainbyte + + @staticmethod + def decrypt(ciphertext: bytes) -> str: + """Decrypt a response of a TP-Link Smart Home Device. + + :param ciphertext: encrypted response data + :return: plaintext response + """ + return bytes( + OriginalTPLinkSmartHomeProtocol._xor_encrypted_payload(ciphertext) + ).decode()