adumont / tplink-cloud-api

A node.js npm module to remotely control TP-Link smartplugs (HS100, HS110) and smartbulbs (LB100, LB110, LB120, LB130) using their cloud web service (no need to be on the same wifi/lan)
https://itnerd.space
GNU General Public License v3.0
129 stars 44 forks source link

Any support of HS300 in the future? #27

Closed montiquewillis closed 4 years ago

montiquewillis commented 5 years ago

I get back the device which is a PowerStrip in the device list. However, the object does not include an array of the underlying outlets. I understand this package doesn't include this device, just wanted to ask. I removed pertinent private values from the below JSON.

{ fwVer: "1.0.10 Build 190103 Rel.163517" deviceName: "Wi-Fi Smart Power Strip" alias: "TP-LINK_Power Strip_CDD2" appServerUrl: "https://use1-wap.tplinkcloud.com" deviceHwVer: "1.0" deviceId: "" deviceMac: "" deviceModel: "HS300(US)" deviceName: "Wi-Fi Smart Power Strip" deviceType: "IOT.SMARTPLUGSWITCH" fwId: "" fwVer: "" hwId: "" isSameRegion: true oemId: "" role: 0 status: 1 }

adumont commented 5 years ago

I understand the HS300 is a strip of 6 independant plugs. How do you see it in the Kasa App? I mean can you set for example an alias for each of the 6 plug? The sample output above seem to show only 1 device (1 deviceId you have stripped). Do you see 6 of these? or only 1 for the 6? That would help me understand (I don't have any HS300 unfortunately)

montiquewillis commented 5 years ago

There are 6 of them in the app with individual switch and energy monitor data. In the app, you can set alias names for each outlet. I'm not able to see a way to change the alias for the strip itself via the app. When I use the getDeviceList() function that's the object I get back. I'm thinking it may be in the original payload, just not in the returned object here, but I could be wrong.

athenawisdoms commented 5 years ago

I am supporting the addition of HS300 support to this amazing library! Is there anything we owners of H300 can do to help with getting HS300 supported, short of doing the actual coding (which is beyond me right now)?

ColinMcNeil commented 5 years ago

In tplink.ts, the code to get the device list is return (this.deviceList = response.data.result.deviceList);. My guess is that there is more information in response, or perhaps there is a different query to get each outlet. I'm guessing there is a 1-M db relationship somewhere that we have to pull from.

atkinsj commented 4 years ago

Adding some additional info here for if you ever get around to it.

Here's what a hit to get the getDeviceList endpoint shows:

{
  "error_code": 0,
  "result": {
    "deviceList": [
      {
        "deviceType": "IOT.SMARTPLUGSWITCH",
        "role": 0,
        "fwVer": "1.0.19 Build 200224 Rel.090814",
        "appServerUrl": "https://use1-wap.tplinkcloud.com",
        "deviceRegion": "us-east-1",
        "deviceId": "redacted",
        "deviceName": "Wi-Fi Smart Power Strip",
        "deviceHwVer": "1.0",
        "alias": "TP-LINK_Power Strip_2A54",
        "deviceMac": "redacted",
        "oemId": "5C9E6254BEBAED63B2B6102966D24C17",
        "deviceModel": "HS300(US)",
        "hwId": "redacted",
        "fwId": "00000000000000000000000000000000",
        "isSameRegion": true,
        "status": 1
      }
    ]
  }
}

It shows as a single device, the 6 port switch.

The relay status method, {"method":"passthrough", "params": {"deviceId": "redacted", "requestData": "{\"system\":{\"get_sysinfo\":null},\"emeter\":{\"get_realtime\":null}}" }}, spits back this:

{
  "system": {
    "get_sysinfo": {
      "sw_ver": "1.0.19 Build 200224 Rel.090814",
      "hw_ver": "1.0",
      "model": "HS300(US)",
      "deviceId": "redacted",
      "oemId": "5C9E6254BEBAED63B2B6102966D24C17",
      "hwId": "redacted",
      "rssi": -59,
      "longitude_i": -1224174,
      "latitude_i": 377759,
      "alias": "TP-LINK_Power Strip_2A54",
      "status": "new",
      "mic_type": "IOT.SMARTPLUGSWITCH",
      "feature": "TIM:ENE",
      "mac": "redacted",
      "updating": 0,
      "led_off": 0,
      "children": [
        {
          "id": "8006070807D6C11B3D3B5B61455944531BF7C62200",
          "state": 1,
          "alias": "Plug 1",
          "on_time": 99381,
          "next_action": {
            "type": -1
          }
        },
        {
          "id": "8006070807D6C11B3D3B5B61455944531BF7C62201",
          "state": 1,
          "alias": "Plug 2",
          "on_time": 254051,
          "next_action": {
            "type": -1
          }
        },
        {
          "id": "8006070807D6C11B3D3B5B61455944531BF7C62202",
          "state": 1,
          "alias": "Plug 3",
          "on_time": 254051,
          "next_action": {
            "type": -1
          }
        },
        {
          "id": "8006070807D6C11B3D3B5B61455944531BF7C62203",
          "state": 1,
          "alias": "Plug 4",
          "on_time": 174221,
          "next_action": {
            "type": -1
          }
        },
        {
          "id": "8006070807D6C11B3D3B5B61455944531BF7C62204",
          "state": 1,
          "alias": "Plug 5",
          "on_time": 9718,
          "next_action": {
            "type": 1,
            "schd_sec": 75600,
            "action": 0
          }
        },
        {
          "id": "8006070807D6C11B3D3B5B61455944531BF7C62205",
          "state": 1,
          "alias": "Plug 6",
          "on_time": 13318,
          "next_action": {
            "type": 1,
            "schd_sec": 72000,
            "action": 0
          }
        }
      ],
      "child_num": 6,
      "err_code": 0
    }
  },
  "emeter": {
    "get_realtime": {
      "voltage_mv": 123640,
      "current_ma": 5,
      "power_mw": 83,
      "total_wh": 311,
      "err_code": 0
    }
  }
}

I suspect there's an additional endpoint to provide those id fields in to in order to change the state. I'll need to spin up an emulator to try and pull this out but was hoping someone else might have already done the heavy lifting.

atkinsj commented 4 years ago

Nevermind, simply using the id field of the individual ports in the usual plug on/plug off method works just fine.

adumont commented 4 years ago

Hi,

Thanks for the info.

This week-end I added support to retrieve a device by its deviceId (before that, you could only get it by its alias).

You might be able to use:

response = await myPlug.getSysInfo();

to get the same output you got with all the info of the children plugs.

Then, can you test something like that?

let myPlug1 = tplink.getHS100("8006070807D6C11B3D3B5B61455944531BF7C62200"); let response = await myPlug1.toggle();

and see if that works?

(Anyway I see all the children also have an alias, so you might be able to also use that directly?)

Does it work if you also try with the alias, instead of the deviceId? I mean:

let myPlug1 = tplink.getHS100("Plug 1"); let response = await myPlug1.toggle();

-- Alex

On Mon, Jun 8, 2020 at 12:53 AM atkinsj notifications@github.com wrote:

Nevermind, simply using the id field of the individual ports in the usual plug on/plug off method works just fine.

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/adumont/tplink-cloud-api/issues/27#issuecomment-640291968, or unsubscribe https://github.com/notifications/unsubscribe-auth/AACRLWSXE34BDBMIVNN4YNDRVQK6RANCNFSM4G6XHDLQ .

atkinsj commented 4 years ago

So turns out I spoke too soon. The children device ID's don't work with any of the standard plug commands, issuing the usual {"error_code":-20580,"msg":"Account is not binded to the device"}. I suspect there's a different parameter you need to provide here but I've been unable to determine what it is. The Android application SSL certificate pins and my usual Frida pinning bypass script isn't working, so I haven't been able to MITM the calls to find the underlying params.

atkinsj commented 4 years ago

TLDR:

curl -i -v --request POST "https://use1-wap.tplinkcloud.com/?token=redacted-token HTTP/1.1" --data '{"method":"passthrough", "params": {"deviceId": "redacted device id of the hs300", "requestData": "{\"context\":{\"child_ids\":[\"redacted sub device id of the port\"],\"source\":\"redacted client id\"},\"system\":{\"set_relay_state\":{\"state\":1}}}", }}' --header "Content-Type: application/json"

Alright, I got to the bottom of this. For anyone curious I spun up the Kasa app in an Android emulator and used Frida to hook like below:

Java.perform(function() {
    var iotrequest = Java.use("com.tplinkra.iot.IOTRequest");

    iotrequest.getData.implementation = function() {
        console.log("getData()")
        console.log(this.toJSON());

        return this.getData();
    }
},0);

This prints the JSON body of every request the application sends.

The application is actually sending quite a bit more detail than this API and the documentation on your blog so they could easily detect non-application usage, I don't think it's overly important though.

Here's the full request body for switching off port 6 on my HS300. You don't need this, the minimum TLDR at the top works.

{
  "data": {
    "deviceId": "redacted",
    "requestData": "{\"context\":{\"child_ids\":[\"redacted\"],\"source\":\"redacted\"},\"system\":{\"set_relay_state\":{\"state\":1}}}",
    "appServerUrl": "https://n-use1-wap.tplinkcloud.com",
    "uri": "com.tplinkra.tplink.appserver.impl.PassthroughRequest"
  },
  "iotContext": {
    "deviceContext": {
      "boundEmail": "redacted",
      "deviceAddress": "redacted",
      "deviceAlias": "Light",
      "deviceCategory": "switch",
      "deviceId": "redacted",
      "deviceModel": "HS300(US)",
      "deviceState": {
        "connectionUnavailableCount": 0,
        "deviceTimeDifference": 0,
        "feature": "TIM:ENE",
        "ignoreConnectionUnavailableTracking": false,
        "isUpdating": false,
        "lastConnectionUnavailableTime": 0,
        "latitude": redacted,
        "longitude": redacted,
        "nextAction": {
          "action": 0,
          "id": "redacted",
          "scheduleTime": 75600,
          "type": 1
        },
        "typeUri": "com.tplinkra.iot.devices.smartplug.impl.SmartPlugDeviceState"
      },
      "deviceType": "IOT.SMARTPLUGSWITCH",
      "hardwareId": "redacted",
      "hardwareVersion": "1.0",
      "isBoundToCloud": true,
      "isLocal": false,
      "isRemote": true,
      "isSameRegion": true,
      "model": "HS300",
      "oemId": "5C9E6254BEBAED63B2B6102966D24C17",
      "parentDeviceContext": {
        "appServerUrl": "https://n-use1-wap.tplinkcloud.com",
        "boundEmail": "redacted",
        "cloudStatus": 1,
        "deviceAddress": "redacted",
        "deviceAlias": "TP-LINK_Power Strip_2A54",
        "deviceCategory": "switch",
        "deviceId": "redacted",
        "deviceModel": "HS300(US)",
        "deviceState": {
          "connectionUnavailableCount": 0,
          "ignoreConnectionUnavailableTracking": false,
          "isUpdating": false,
          "lastConnectionUnavailableTime": 0,
          "latitude": redacted,
          "longitude": redacted,
          "typeUri": "com.tplinkra.iot.devices.common.ClassADeviceState"
        },
        "deviceType": "IOT.SMARTPLUGSWITCH",
        "hardwareId": "redacted",
        "hardwareVersion": "1.0",
        "isBoundToCloud": true,
        "isParent": true,
        "isRemote": true,
        "isSameRegion": true,
        "model": "HS300",
        "oemId": "redacted",
        "rssi": -56,
        "softwareVersion": "1.0.19 Build 200224 Rel.090814"
      },
      "rssi": -56,
      "softwareVersion": "1.0.19 Build 200224 Rel.090814"
    },
    "userContext": {
      "accountToken": "redacted",
      "app": {
        "appClientId": "redacted",
        "appType": "Kasa_Android"
      },
      "email": "redacted",
      "terminalId": "redacted"
    }
  },
  "method": "passthrough",
  "requestId": "redacted"
}
adumont commented 4 years ago

Thank you very much for the help.

This Frida method might definitely prove useful. Indeed the Kasa API looks more verbose than it was some years ago, probably due to the fact that they now support a lot more devices. Still, they haven't broken compatibility, so far, with the API.

I'll have a look at what you send when I get some time.

BTW Anyone is welcome to contribute if they feel up to it, don't hesitate to send a Pull Request when you have something working so I can merge it in the NPM module.

adumont commented 4 years ago

@atkinsj , in your TL;DR, what is \"source\":\"redacted client id\" ? What does it represent here? Is it a required parameter or does it still work if you omit it?

atkinsj commented 4 years ago

It's the application client ID, the uuid4 that you generate. It works when omitted.

adumont commented 4 years ago

I don't have a HS300, but I have tried with all the stuff here to come up with something. It might work, or not... If anyone wants to try, it's in the releases: https://github.com/adumont/tplink-cloud-api/releases/tag/v0.6.2-next.0 (2ab601c)

you could try with:

require('dotenv').config()
const { login } = require("tplink-cloud-api");
const uuidV4 = require("uuid/v4");

// define these 3 variables in your .env file
// you can copy .env.sample to .env and edit it
const TPLINK_USER = process.env.TPLINK_USER;
const TPLINK_PASS = process.env.TPLINK_PASS;

// you can optionally define this one, or it will get a value
const TPLINK_TERM = process.env.TPLINK_TERM || uuidV4();

async function main() {
    // log in to cloud, return a connected tplink object
    const tplink = await login(TPLINK_USER, TPLINK_PASS, TPLINK_TERM);

    // get a list of raw json objects (must be invoked before .get* works)
    const dl = await tplink.getDeviceList();
    console.log("DeviceList:", dl);

    let myPlug = tplink.getHS300("my plug");
    let children = await myPlug.getChildren();
    console.log("response",myPlug.findChild("Plug 5"));

    let child5 = myPlug.getChild("Plug 5");
    child5.powerOff();

    console.log("getSysInfo: ", await child5.getSysInfo() );
}

main();

don't hesitate to try all the methods of hs300child (see https://github.com/adumont/tplink-cloud-api/blob/v0.6.2-next.0/lib/hs300child.ts) and report if there any failure or success. :)

adumont commented 4 years ago

Sorry, just realized I've left some bogus data in the code, so don't bother to test it, it will fail :(. I'll have to remove it, repack and upload a new alpha release later when I find some time.

adumont commented 4 years ago

Hopefully I have fixed the package, and you should be able to test if for HS300 support. You can download it from: https://github.com/adumont/tplink-cloud-api/releases/tag/0.6.2-next.2 , and test with code like the one I put in my comment above.

adumont commented 4 years ago

Were you able to test the version I posted? Hopefully it should work for the HS300 (and support childs).

Alex

On Wed, Jun 10, 2020 at 2:34 AM atkinsj notifications@github.com wrote:

It's the application client ID, the uuid4 that you generate. It works when omitted.

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/adumont/tplink-cloud-api/issues/27#issuecomment-641651379, or unsubscribe https://github.com/notifications/unsubscribe-auth/AACRLWTD74632HSD33ZJZELRV3ILDANCNFSM4G6XHDLQ .

piekstra commented 4 years ago

@adumont I've been using your repo for a project and the child logic you wrote up has been working for me. I haven't tested your code directly, as I've been porting your logic over to Python 3, but it worked in the ported code.

Also, if you could use someone to run some tests on other device types, I have a number of Kasa models I'm using and would gladly help to contribute. (HS103, HS105, HS300)

adumont commented 4 years ago

@adumont I've been using your repo for a project and the child logic you wrote up has been working for me. I haven't tested your code directly, as I've been porting your logic over to Python 3, but it worked in the ported code.

Also, if you could use someone to run some tests on other device types, I have a number of Kasa models I'm using and would gladly help to contribute. (HS103, HS105, HS300)

@piekstra Could you try the HS300 support in this version https://github.com/adumont/tplink-cloud-api/releases/tag/0.6.2-next.2 ? (see example code a few comments above, in https://github.com/adumont/tplink-cloud-api/issues/27#issuecomment-642304749)

piekstra commented 4 years ago

@adumont I've been using your repo for a project and the child logic you wrote up has been working for me. I haven't tested your code directly, as I've been porting your logic over to Python 3, but it worked in the ported code. Also, if you could use someone to run some tests on other device types, I have a number of Kasa models I'm using and would gladly help to contribute. (HS103, HS105, HS300)

@piekstra Could you try the HS300 support in this version https://github.com/adumont/tplink-cloud-api/releases/tag/0.6.2-next.2 ? (see example code a few comments above, in #27 (comment))

@adumont Worked successfully! I ran with this sample code

async function doThings() {
    const { login } = require("../tplink-cloud-api-0.6.2-next.2");
    const tplink = await login("redacted@email.com", "redacted");

    // get a list of raw json objects (must be invoked before .get* works)
    const dl = await tplink.getDeviceList();
    console.log("DeviceList:", dl);

    let powerStripName = "TP-LINK_Power Strip_0AB1"
    let powerStrip = tplink.getHS300(powerStripName);

    let children = await powerStrip.getChildren();
    console.log("Children of ", powerStripName, " : ", children)

    let child5 = powerStrip.getChild("Test 5");
    console.log("child5 response: ", child5);

    let powerOnResult = await child5.powerOn();
    console.log("powerOnResult: ", powerOnResult)

    let powerOffResult = await child5.powerOff();
    console.log("powerOffResult: ", powerOffResult)

    console.log("getSysInfo: ", await child5.getSysInfo() );
}

doThings();

It successfully read the data and also turned the individual plug on and back off

cleaned_results.txt

adumont commented 4 years ago

Great @piekstra. Thank you for taking time to test it. I've published a new updated version (0.8 on npm, including the hs300 support).

e52f75d0ac015b44cb1a799db71040bc9ec63624