// // Copyright 2015-2021 by Garmin Ltd. or its subsidiaries. // Subject to Garmin SDK License Agreement and Wearables // Application Developer Agreement. // import Toybox.Cryptography; import Toybox.Lang; class Tplink { private var api; private var token; private var ctrl_cmd as Dictionary or Null; private var cur_dev as Dictionary or Null; private var enumerated as Array or Null = null; private var enumerated_idx; private var enumerated_list; public function initialize() { L.dbg_log(L.DBG_HTTP, "Tplink.initialize()"); api = new TpApi(C.settings["tp_svr"], C.settings["tp_usr"], C.settings["tp_pwd"]); } public function avail() { return !C.empty(C.settings["tp_svr"]) && !C.empty(C.settings["tp_usr"]); } private function tp_type(name) { var icon = G.ICON_PLUG; if (name.find("Switch") != null) { icon = G.ICON_SWITCH; } else if (name.find("Strip") != null) { icon = G.ICON_STRIP; } else if (name.find("Bulb") != null) { icon = G.ICON_BULB; } return icon; } private function children(info as Dictionary or Null, parent, url) { var child; if (info == null) { return; } var childs = info["children"] as Dictionary or Null; if (childs == null) { return; } var str = ""; for (var j = 0; j < childs.size(); j += 1) { child = childs[j] as Dictionary; C.outlets.add({ "type" => C.OUTLET_TPLINK, "name" => child["alias"], "icon" => G.ICON_PLUG, "state" => child["state"], "ood" => false, "info" => { "url" => url, "id" => child["id"], "parent" => parent } }); str += "," + child["alias"]; } L.dbg_log(L.DBG_HTTP, "Tplink children" + str); } public function tp_add(name, icon, state, url, id, parent) { C.set_alert(null, name); tp_get({ "type" => C.OUTLET_TPLINK, "name" => name, "icon" => icon, "state" => G.STATE_OFL, "ood" => false, "info" => { "url" => url, "id" => id, "parent" => parent } }); return name; } function dev_name(name, type) { if ((type == null) || (type.find("IOT.SMART") != null)) { return name; } var b64 = C.b64_decode(name); return (b64 == null) ? name : b64; } public function enumerate_devs() { var dev, name, type; while (enumerated_idx < enumerated.size()) { dev = enumerated[enumerated_idx] as Dictionary; enumerated_idx += 1; name = dev_name(dev["alias"], dev["deviceType"]); type = tp_type(dev["deviceName"]); enumerated_list += "," + tp_add(name, type, G.STATE_OFL, dev["appServerUrl"], dev["deviceId"], null); return; } L.dbg_log(L.DBG_HTTP, "Tplink device" + enumerated_list); enumerated = null; ctrl_cmd["cb"].invoke(C.OUTLET_TPLINK + 1, null); } public function tp_enumerate(code, data as Dictionary) { if (code != 200) { L.dbg_log(L.DBG_HTTP, "tplink device:failed - " + code); C.state = C.MAIN_ERROR; ctrl_cmd["cb"].invoke(C.OUTLET_ERROR, C.err_msg(code)); return; } var json = data["result"] as Dictionary; enumerated = json["deviceList"] as Array; enumerated_idx = 0; enumerated_list = ""; enumerate_devs(); } public function enumerate() { var body = { "method" => "getDeviceList" }; api.post("?token=" + token, body, method(:tp_enumerate)); } public function tp_login(code as Number, body as Dictionary or String or Null) as Void { var data = body as Dictionary; if (code != 200) { L.dbg_log(L.DBG_BASIC, "login failed:" + code); C.state = C.MAIN_ERROR; ctrl_cmd["cb"].invoke(C.OUTLET_ERROR, C.err_msg(code)); return; } try { if (data["error_code"] == -20004) { ctrl_cmd["cb"].invoke(C.OUTLET_ERROR, "Rate limited"); return; } var json = data["result"] as Dictionary; token = json["token"] as Dictionary; } catch(e) { ctrl_cmd["cb"].invoke(C.OUTLET_ERROR, "Bad usr/pwd"); return; } do_ctrl(); } public function login() { L.dbg_log(L.DBG_BASIC, "Tplink:login(" + C.settings["tp_usr"] + ", ...)"); if (api.get_host() == null) { ctrl_cmd["cb"].invoke(C.OUTLET_TPLINK + 1, "no host"); return; } var body = { "method" => "login", "params" => { "appType" => "Kasa_Android", "cloudUserName" => C.settings["tp_usr"], "cloudPassword" => C.settings["tp_pwd"], "terminalUUID" => C.fake_uuid() } }; api.post("?locale=en_US", body, method(:tp_login)); } private function child_state(id, info as Dictionary) { var child; var childs = info["children"] as Dictionary or Null; if (childs == null) { return 0; } for (var j = 0; j < childs.size(); j += 1) { child = childs[j] as Dictionary; if (id.equals(child["id"])) { return child["state"]; } } return 0; } private function tp_state(dev as Dictionary, info as Dictionary or Null) { var state, l_state, dev_info; if (info == null) { return G.STATE_UNK; } if (info["err_code"] == 0) { dev_info = dev["info"] as Dictionary; state = 0; if (dev_info["parent"] != null) { state = child_state(dev_info["id"], info); } else if (info["light_state"] != null) { l_state = info["light_state"] as Dictionary; state = l_state["on_off"]; } else if (info["relay_state"] != null) { state = info["relay_state"]; } return (state == 0) ? G.STATE_OFF : G.STATE_ON; } return G.STATE_OFL; } private function tp_found(code, info as Dictionary) { // // The original call to tp_get() will save the desired // device in cur_dev // var state = tp_state(cur_dev, info); cur_dev["state"] = (state == G.STATE_UNK) ? G.STATE_OFL : state; C.outlets.add(cur_dev); var tp_info = cur_dev["info"] as Dictionary; children(info, tp_info["id"], tp_info["url"]); enumerate_devs(); } // // Garmin has too many error codes (at least an error_code for the response // and an err_code for the data info that I know of). Rather than try an // handle every error code just assume that the correct relay state is returned // and, if not, let the try/except handle the end cases // public function tp_got(code, data as Dictionary) { var info = null; if (code == 200) { try { info = (((data["result"] as Dictionary)["responseData"] as Dictionary)["system"] as Dictionary)["get_sysinfo"] as Dictionary; } catch(e) { info = null; } } if (enumerated == null) { var state = tp_state(cur_dev, info); ctrl_cmd["cb"].invoke(tp_state(cur_dev, info), ((state == G.STATE_UNK) ? "state unknown" : null)); } else { tp_found(code, info); } } public function tp_get(dev as Dictionary) { var body; cur_dev = dev; var info = dev["info"] as Dictionary; var parent = info["parent"]; var id = (parent != null) ? parent : info["id"]; body = { "method" => "passthrough", "params" => { "deviceId" => id, "requestData" => { "system" => { "get_sysinfo" => null } } } }; api.set_host(info["url"]); api.post("?token=" + token, body, method(:tp_got)); } public function tp_set(code, data as Dictionary) { if (code == 200) { ctrl_cmd["cb"].invoke(ctrl_cmd["state"], (data["error_code"] == 0 ? null : "network error")); return; } ctrl_cmd["cb"].invoke(ctrl_cmd["state"], "Error - " + code); } public function tp_onoff() { var body; var dev = ctrl_cmd["device"] as Dictionary; var info = dev["info"] as Dictionary; var parent = info["parent"]; if (dev["icon"] == G.ICON_BULB) { body = { "method" => "passthrough", "params" => { "deviceId" => info["id"], "requestData" => { "smartlife.iot.smartbulb.lightingservice" => { "transition_light_state" => { "on_off" => ctrl_cmd["state"], "transition_period" => 1 } } } } }; } else if (parent != null) { body = { "method" => "passthrough", "params" => { "deviceId" => parent, "requestData" => { "context" => { "child_ids" => [ info["id"] ] }, "system" => { "set_relay_state" => { "state" => ctrl_cmd["state"] } } } } }; } else { body = { "method" => "passthrough", "params" => { "deviceId" => info["id"], "requestData" => { "system" => { "set_relay_state" => { "state" => ctrl_cmd["state"] } } } } }; } //L.pr_json(L.DBG_HTTP, "tp_onoff", body); api.set_host(info["url"]); api.post("?token=" + token, body, method(:tp_set)); } public function do_ctrl() { var cmd = ctrl_cmd["cmd"]; switch (cmd) { case C.CTRL_ENUMERATE: enumerate(); return; case C.CTRL_GET: tp_get(ctrl_cmd["device"]); return; case C.CTRL_SET: tp_onoff(); return; } L.dbg_log(L.DBG_BASIC, "ctrl:" + "unknown command - " + cmd); ctrl_cmd["cb"].invoke(cmd, "unknown command"); } public function ctrl(cmd, device as Dictionary, state, cb) { ctrl_cmd = { "cmd" => cmd, "device" => device, "state" => state, "cb" => cb }; if (token == null) { login(); } else { do_ctrl(); } } }