From caf7149791c71addccdd05ce709c580c8070325e Mon Sep 17 00:00:00 2001 From: mrbetta Date: Wed, 8 Dec 2021 20:12:28 -0700 Subject: [PATCH 1/5] Added motion and light sensor for KS220M --- dump_devinfo.py | 149 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 dump_devinfo.py diff --git a/dump_devinfo.py b/dump_devinfo.py new file mode 100644 index 000000000..55217426b --- /dev/null +++ b/dump_devinfo.py @@ -0,0 +1,149 @@ +"""This script generates devinfo files for the test suite. + +If you have new, yet unsupported device or a device with no devinfo file under kasa/tests/fixtures, +feel free to run this script and create a PR to add the file to the repository. + +Executing this script will several modules and methods one by one, +and finally execute a query to query all of them at once. +""" +import asyncio +import collections.abc +import json +import logging +import re +from collections import defaultdict, namedtuple +from pprint import pprint + +import click + +from kasa import TPLinkSmartHomeProtocol + +Call = namedtuple("Call", "module method") + + +def scrub(res): + """Remove identifiers from the given dict.""" + keys_to_scrub = [ + "deviceId", + "fwId", + "hwId", + "oemId", + "mac", + "mic_mac", + "latitude_i", + "longitude_i", + "latitude", + "longitude", + ] + + for k, v in res.items(): + if isinstance(v, collections.abc.Mapping): + res[k] = scrub(res.get(k)) + else: + if k in keys_to_scrub: + if k in ["latitude", "latitude_i", "longitude", "longitude_i"]: + v = 0 + else: + v = re.sub(r"\w", "0", v) + + res[k] = v + return res + + +def default_to_regular(d): + """Convert nested defaultdicts to regular ones. + + From https://stackoverflow.com/a/26496899 + """ + if isinstance(d, defaultdict): + d = {k: default_to_regular(v) for k, v in d.items()} + return d + + +@click.command() +@click.argument("host") +@click.option("-d", "--debug", is_flag=True) +def cli(host, debug): + """Generate devinfo file for given device.""" + if debug: + logging.basicConfig(level=logging.DEBUG) + + items = [ + Call(module="system", method="get_sysinfo"), + Call(module="emeter", method="get_realtime"), + Call(module="smartlife.iot.dimmer", method="get_dimmer_parameters"), + Call(module="smartlife.iot.common.emeter", method="get_realtime"), + Call(module="smartlife.iot.LAS", method="get_config"), + Call(module="smartlife.iot.PIR", method="get_config"), + + Call( + module="smartlife.iot.smartbulb.lightingservice", method="get_light_state" + ), + ] + + successes = [] + + for test_call in items: + + async def _run_query(): + protocol = TPLinkSmartHomeProtocol(host) + return await protocol.query({test_call.module: {test_call.method: None}}) + + try: + click.echo(f"Testing {test_call}..", nl=False) + info = asyncio.run(_run_query()) + resp = info[test_call.module] + except Exception as ex: + click.echo(click.style(f"FAIL {ex}", fg="red")) + else: + if "err_msg" in resp: + click.echo(click.style(f"FAIL {resp['err_msg']}", fg="red")) + else: + click.echo(click.style("OK", fg="green")) + successes.append((test_call, info)) + + final_query = defaultdict(defaultdict) + final = defaultdict(defaultdict) + + for succ, resp in successes: + final_query[succ.module][succ.method] = None + final[succ.module][succ.method] = resp + + final = default_to_regular(final) + + async def _run_final_query(): + protocol = TPLinkSmartHomeProtocol(host) + return await protocol.query(final_query) + + try: + final = asyncio.run(_run_final_query()) + except Exception as ex: + click.echo( + click.style( + f"Unable to query all successes at once: {ex}", bold=True, fg="red" + ) + ) + + click.echo("Got %s successes" % len(successes)) + click.echo(click.style("## device info file ##", bold=True)) + + sysinfo = final["system"]["get_sysinfo"] + model = sysinfo["model"] + hw_version = sysinfo["hw_ver"] + sw_version = sysinfo["sw_ver"] + sw_version = sw_version.split(" ", maxsplit=1)[0] + save_to = f"{model}_{hw_version}_{sw_version}.json" + pprint(scrub(final)) + save = click.prompt(f"Do you want to save the above content to {save_to} (y/n)") + if save == "y": + click.echo(f"Saving info to {save_to}") + + with open(save_to, "w") as f: + json.dump(final, f, sort_keys=True, indent=4) + f.write("\n") + else: + click.echo("Not saving.") + + +if __name__ == "__main__": + cli() From b77c6e3b66873184ba8476611397e7c52bc47d28 Mon Sep 17 00:00:00 2001 From: mrbetta Date: Wed, 8 Dec 2021 20:24:09 -0700 Subject: [PATCH 2/5] Added fixture file for ks220m --- kasa/tests/fixtures/KS220M(US)_1.0_1.0.4.json | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 kasa/tests/fixtures/KS220M(US)_1.0_1.0.4.json diff --git a/kasa/tests/fixtures/KS220M(US)_1.0_1.0.4.json b/kasa/tests/fixtures/KS220M(US)_1.0_1.0.4.json new file mode 100644 index 000000000..40da46fdd --- /dev/null +++ b/kasa/tests/fixtures/KS220M(US)_1.0_1.0.4.json @@ -0,0 +1,126 @@ +{ + "smartlife.iot.LAS": { + "get_config": { + "devs": [ + { + "dark_index": 0, + "enable": 1, + "hw_id": 0, + "level_array": [ + { + "adc": 490, + "name": "cloudy", + "value": 20 + }, + { + "adc": 294, + "name": "overcast", + "value": 12 + }, + { + "adc": 222, + "name": "dawn", + "value": 9 + }, + { + "adc": 222, + "name": "twilight", + "value": 9 + }, + { + "adc": 111, + "name": "total darkness", + "value": 4 + }, + { + "adc": 2400, + "name": "custom", + "value": 97 + } + ], + "max_adc": 2450, + "min_adc": 0 + } + ], + "err_code": 0, + "ver": "1.0" + } + }, + "smartlife.iot.PIR": { + "get_config": { + "array": [ + 80, + 50, + 20, + 61 + ], + "cold_time": 60000, + "enable": 1, + "err_code": 0, + "max_adc": 4095, + "min_adc": 0, + "trigger_index": 2, + "version": "1.0" + } + }, + "smartlife.iot.dimmer": { + "get_dimmer_parameters": { + "bulb_type": 1, + "err_code": 0, + "fadeOffTime": 0, + "fadeOnTime": 0, + "gentleOffTime": 10000, + "gentleOnTime": 3000, + "minThreshold": 1, + "rampRate": 30 + } + }, + "system": { + "get_sysinfo": { + "active_mode": "none", + "alias": "Garage Entryway Lights", + "brightness": 100, + "dev_name": "Wi-Fi Smart Dimmer with sensor", + "deviceId": "0000000000000000000000000000000000000000", + "err_code": 0, + "feature": "TIM", + "hwId": "00000000000000000000000000000000", + "hw_ver": "1.0", + "icon_hash": "", + "latitude_i": 0, + "led_off": 0, + "longitude_i": 0, + "mac": "00:00:00:00:00:00", + "mic_type": "IOT.SMARTPLUGSWITCH", + "model": "KS220M(US)", + "next_action": { + "type": -1 + }, + "oemId": "00000000000000000000000000000000", + "on_time": 0, + "preferred_state": [ + { + "brightness": 100, + "index": 0 + }, + { + "brightness": 75, + "index": 1 + }, + { + "brightness": 50, + "index": 2 + }, + { + "brightness": 25, + "index": 3 + } + ], + "relay_state": 0, + "rssi": -42, + "status": "new", + "sw_ver": "1.0.4 Build 210616 Rel.193517", + "updating": 0 + } + } +} From da9f532eb6d931986b8ec9a96ece7b5d7f804296 Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Sat, 29 Jan 2022 18:08:37 +0100 Subject: [PATCH 3/5] Remove dump_devinfo and add the extra queries to devtools/dump_devinfo --- devtools/dump_devinfo.py | 2 + dump_devinfo.py | 149 --------------------------------------- 2 files changed, 2 insertions(+), 149 deletions(-) delete mode 100644 dump_devinfo.py diff --git a/devtools/dump_devinfo.py b/devtools/dump_devinfo.py index 1108e7fb4..2f3c20e01 100644 --- a/devtools/dump_devinfo.py +++ b/devtools/dump_devinfo.py @@ -76,6 +76,8 @@ def cli(host, debug): Call( module="smartlife.iot.smartbulb.lightingservice", method="get_light_state" ), + Call(module="smartlife.iot.LAS", method="get_config"), + Call(module="smartlife.iot.PIR", method="get_config"), ] successes = [] diff --git a/dump_devinfo.py b/dump_devinfo.py deleted file mode 100644 index 55217426b..000000000 --- a/dump_devinfo.py +++ /dev/null @@ -1,149 +0,0 @@ -"""This script generates devinfo files for the test suite. - -If you have new, yet unsupported device or a device with no devinfo file under kasa/tests/fixtures, -feel free to run this script and create a PR to add the file to the repository. - -Executing this script will several modules and methods one by one, -and finally execute a query to query all of them at once. -""" -import asyncio -import collections.abc -import json -import logging -import re -from collections import defaultdict, namedtuple -from pprint import pprint - -import click - -from kasa import TPLinkSmartHomeProtocol - -Call = namedtuple("Call", "module method") - - -def scrub(res): - """Remove identifiers from the given dict.""" - keys_to_scrub = [ - "deviceId", - "fwId", - "hwId", - "oemId", - "mac", - "mic_mac", - "latitude_i", - "longitude_i", - "latitude", - "longitude", - ] - - for k, v in res.items(): - if isinstance(v, collections.abc.Mapping): - res[k] = scrub(res.get(k)) - else: - if k in keys_to_scrub: - if k in ["latitude", "latitude_i", "longitude", "longitude_i"]: - v = 0 - else: - v = re.sub(r"\w", "0", v) - - res[k] = v - return res - - -def default_to_regular(d): - """Convert nested defaultdicts to regular ones. - - From https://stackoverflow.com/a/26496899 - """ - if isinstance(d, defaultdict): - d = {k: default_to_regular(v) for k, v in d.items()} - return d - - -@click.command() -@click.argument("host") -@click.option("-d", "--debug", is_flag=True) -def cli(host, debug): - """Generate devinfo file for given device.""" - if debug: - logging.basicConfig(level=logging.DEBUG) - - items = [ - Call(module="system", method="get_sysinfo"), - Call(module="emeter", method="get_realtime"), - Call(module="smartlife.iot.dimmer", method="get_dimmer_parameters"), - Call(module="smartlife.iot.common.emeter", method="get_realtime"), - Call(module="smartlife.iot.LAS", method="get_config"), - Call(module="smartlife.iot.PIR", method="get_config"), - - Call( - module="smartlife.iot.smartbulb.lightingservice", method="get_light_state" - ), - ] - - successes = [] - - for test_call in items: - - async def _run_query(): - protocol = TPLinkSmartHomeProtocol(host) - return await protocol.query({test_call.module: {test_call.method: None}}) - - try: - click.echo(f"Testing {test_call}..", nl=False) - info = asyncio.run(_run_query()) - resp = info[test_call.module] - except Exception as ex: - click.echo(click.style(f"FAIL {ex}", fg="red")) - else: - if "err_msg" in resp: - click.echo(click.style(f"FAIL {resp['err_msg']}", fg="red")) - else: - click.echo(click.style("OK", fg="green")) - successes.append((test_call, info)) - - final_query = defaultdict(defaultdict) - final = defaultdict(defaultdict) - - for succ, resp in successes: - final_query[succ.module][succ.method] = None - final[succ.module][succ.method] = resp - - final = default_to_regular(final) - - async def _run_final_query(): - protocol = TPLinkSmartHomeProtocol(host) - return await protocol.query(final_query) - - try: - final = asyncio.run(_run_final_query()) - except Exception as ex: - click.echo( - click.style( - f"Unable to query all successes at once: {ex}", bold=True, fg="red" - ) - ) - - click.echo("Got %s successes" % len(successes)) - click.echo(click.style("## device info file ##", bold=True)) - - sysinfo = final["system"]["get_sysinfo"] - model = sysinfo["model"] - hw_version = sysinfo["hw_ver"] - sw_version = sysinfo["sw_ver"] - sw_version = sw_version.split(" ", maxsplit=1)[0] - save_to = f"{model}_{hw_version}_{sw_version}.json" - pprint(scrub(final)) - save = click.prompt(f"Do you want to save the above content to {save_to} (y/n)") - if save == "y": - click.echo(f"Saving info to {save_to}") - - with open(save_to, "w") as f: - json.dump(final, f, sort_keys=True, indent=4) - f.write("\n") - else: - click.echo("Not saving.") - - -if __name__ == "__main__": - cli() From 836e0cb1d1534bda83b7a871f3080470ff7c7ca4 Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Sat, 29 Jan 2022 18:12:31 +0100 Subject: [PATCH 4/5] Test KS220M as a dimmer --- kasa/tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kasa/tests/conftest.py b/kasa/tests/conftest.py index 2c1d1e49c..f9fc917f6 100644 --- a/kasa/tests/conftest.py +++ b/kasa/tests/conftest.py @@ -53,7 +53,7 @@ "KP401", } STRIPS = {"HS107", "HS300", "KP303", "KP400", "EP40"} -DIMMERS = {"HS220"} +DIMMERS = {"HS220", "KS220M"} DIMMABLE = {*BULBS, *DIMMERS} WITH_EMETER = {"HS110", "HS300", "KP115", *BULBS} From 4bb3615e9cfe0f0451519e4cacf0f45545281ab4 Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Sat, 29 Jan 2022 18:23:17 +0100 Subject: [PATCH 5/5] Add empty modules to baseproto to make the tests pass --- kasa/tests/newfakes.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/kasa/tests/newfakes.py b/kasa/tests/newfakes.py index 2afae2aa3..2e859e362 100644 --- a/kasa/tests/newfakes.py +++ b/kasa/tests/newfakes.py @@ -437,6 +437,8 @@ def light_state(self, x, *args): "set_brightness": set_hs220_brightness, "set_dimmer_transition": set_hs220_dimmer_transition, }, + "smartlife.iot.LAS": {}, + "smartlife.iot.PIR": {}, } async def query(self, request, port=9999):