├── .gitignore ├── .github ├── FUNDING.yml └── workflows │ └── publish.yml ├── src ├── test.js ├── test.integration.js ├── index.js └── tplight.cjs ├── LICENSE ├── package.json ├── tplink-smarthome.lua ├── README.md └── API.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | dist/ -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: konsumer 4 | -------------------------------------------------------------------------------- /src/test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, expect */ 2 | 3 | import TPLSmartDevice from './index.js' 4 | 5 | describe('TPLSmartDevice', () => { 6 | it('should have unit-tests', () => { 7 | expect(1 + 1).toBe(2) 8 | }) 9 | 10 | it('should be able to encrypt', () => { 11 | expect(TPLSmartDevice.encrypt(Buffer.from('hello world')).toString('hex')).toBe('c3a6caa6c9e99ef183ef8b') 12 | }) 13 | 14 | it('should be able to decrypt', () => { 15 | expect(TPLSmartDevice.decrypt(Buffer.from('c3a6caa6c9e99ef183ef8b', 'hex')).toString('utf8')).toBe('hello world') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /src/test.integration.js: -------------------------------------------------------------------------------- 1 | // this requires real hardware 2 | 3 | import TPLSmartDevice from './index.js' 4 | 5 | // get a number of real devices to test 6 | async function getNLights (n = 1, timeout=3000) { 7 | return new Promise((resolve, reject) => { 8 | const scan = TPLSmartDevice.scan() 9 | const testLights = [] 10 | const t = setTimeout(() => { 11 | if (testLights.length < n) { 12 | reject(new Error('Timeout without getting required number of devices')) 13 | } 14 | }, timeout) 15 | scan.on('light', light => { 16 | testLights.push(light) 17 | if (testLights.length >= n) { 18 | clearTimeout(t) 19 | scan.stop() 20 | resolve(testLights) 21 | } 22 | }) 23 | }) 24 | } 25 | 26 | const lights = await getNLights() 27 | console.log(lights) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 David Konsumer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - '*' 5 | 6 | jobs: 7 | publish: 8 | name: Publish 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v1 12 | 13 | - name: Test & Build 14 | uses: actions/setup-node@v1 15 | with: 16 | node-version: 16 17 | 18 | - run: npm ci 19 | - run: npm test 20 | - run: npm run build 21 | - run: npm run release 22 | - run: mkdir -p dist/linux && mv dist/tplight-linux dist/linux/tplight 23 | - run: mkdir -p dist/macos && mv dist/tplight-macos dist/macos/tplight 24 | - run: mkdir -p dist/windows && mv dist/tplight-win.exe dist/windows/tplight.exe 25 | 26 | - name: Publish on NPM 27 | uses: JS-DevTools/npm-publish@v1 28 | with: 29 | token: ${{ secrets.NPM_TOKEN }} 30 | 31 | - name: Release Windows 32 | uses: thedoctor0/zip-release@master 33 | with: 34 | type: 'zip' 35 | filename: 'tplight-windows.zip' 36 | directory: dist/windows 37 | path: tplight.exe 38 | 39 | - name: Release Mac (intel 64) 40 | uses: thedoctor0/zip-release@master 41 | with: 42 | type: 'zip' 43 | filename: 'tplight-macos-i64.zip' 44 | directory: dist/macos 45 | path: tplight 46 | 47 | - name: Release Linux (intel 64) 48 | uses: thedoctor0/zip-release@master 49 | with: 50 | type: 'zip' 51 | filename: 'tplight-linux-i64.zip' 52 | directory: dist/linux 53 | path: tplight 54 | 55 | - name: Publish Releases 56 | uses: ncipollo/release-action@v1 57 | with: 58 | artifacts: "dist/**/*.zip" 59 | token: ${{ secrets.GITHUB_TOKEN }} 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tplink-lightbulb", 3 | "version": "1.8.0", 4 | "type": "module", 5 | "description": "Control TP-Link smart-home devices from nodejs", 6 | "scripts": { 7 | "build": "microbundle --target node", 8 | "test": "NODE_OPTIONS='--experimental-vm-modules --no-warnings' jest", 9 | "test:integration": "node ./src/*.integration.js", 10 | "release": "pkg --public --out-path=./dist ./src/tplight.cjs", 11 | "prepublishOnly": "npm run build" 12 | }, 13 | "bin": { 14 | "tplight": "src/tplight.cjs" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/konsumer/tplink-lightbulb.git" 19 | }, 20 | "author": "David Konsumer ", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/konsumer/tplink-lightbulb/issues" 24 | }, 25 | "homepage": "https://github.com/konsumer/tplink-lightbulb#readme", 26 | "keywords": [ 27 | "tp-link", 28 | "lightbulb", 29 | "smartbulb", 30 | "smart-home", 31 | "LB120", 32 | "LB130", 33 | "HS100", 34 | "HS105", 35 | "HS110", 36 | "KP100" 37 | ], 38 | "dependencies": { 39 | "yargs": "latest" 40 | }, 41 | "source": "./src/index.js", 42 | "exports": { 43 | "require": "./dist/tplink-lightbulb.cjs", 44 | "default": "./dist/tplink-lightbulb.modern.js" 45 | }, 46 | "main": "./dist/tplink-lightbulb.cjs", 47 | "module": "./dist/tplink-lightbulb.module.js", 48 | "unpkg": "./dist/tplink-lightbulb.umd.js", 49 | "files": [ 50 | "src/tplight.cjs", 51 | "src/index.js", 52 | "dist/tplink-lightbulb.cjs", 53 | "dist/tplink-lightbulb.cjs.map", 54 | "dist/tplink-lightbulb.modern.js", 55 | "dist/tplink-lightbulb.modern.js.map", 56 | "dist/tplink-lightbulb.module.js", 57 | "dist/tplink-lightbulb.module.js.map", 58 | "dist/tplink-lightbulb.umd.js", 59 | "dist/tplink-lightbulb.umd.js.map" 60 | ], 61 | "devDependencies": { 62 | "jest": "^28.1.2", 63 | "microbundle": "^0.15.0", 64 | "pkg": "^5.7.0" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tplink-smarthome.lua: -------------------------------------------------------------------------------- 1 | -- TP-Link Smart Home Protocol (Port 9999) Wireshark Dissector 2 | -- For decrypting local network traffic between TP-Link 3 | -- Smart Home Devices and the Kasa Smart Home App 4 | -- 5 | -- Install in the location listed in About Wireshark/Folders/Personal Plugins 6 | -- 7 | -- by Lubomir Stroetmann 8 | -- Copyright 2016 softScheck GmbH 9 | -- 10 | -- Licensed under the Apache License, Version 2.0 (the "License"); 11 | -- you may not use this file except in compliance with the License. 12 | -- You may obtain a copy of the License at 13 | -- 14 | -- http://www.apache.org/licenses/LICENSE-2.0 15 | -- 16 | -- Unless required by applicable law or agreed to in writing, software 17 | -- distributed under the License is distributed on an "AS IS" BASIS, 18 | -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 19 | -- See the License for the specific language governing permissions and 20 | -- limitations under the License. 21 | -- 22 | -- 23 | 24 | -- Create TP-Link Smart Home protocol and its fields 25 | hs1x0_proto_TCP = Proto ("TPLink-SmartHome-TCP", "TP-Link Smart Home Protocol (TCP") 26 | hs1x0_proto_UDP = Proto ("TPLink-SmartHome-UDP", "TP-Link Smart Home Protocol (UDP)") 27 | 28 | -- Decrypt string Autokey XOR to ByteArray 29 | function tpdecode(buf, start) 30 | local key = 171 31 | local size = buf:len()-1 32 | local decoded = "" 33 | for i=start,size do 34 | local c = buf(i,1):uint() 35 | decoded = decoded .. string.format("%x", bit.bxor(c,key)) 36 | key = c 37 | end 38 | return ByteArray.new(decoded) 39 | end 40 | 41 | function hs1x0_proto_TCP.dissector (buf, pkt, root) 42 | pkt.cols.protocol = "TPLink-SmartHome (TCP)" 43 | local subtree = root:add(hs1x0_proto_TCP, buf() ,"TPLink-SmartHome") 44 | local decoded = tpdecode(buf, 4) 45 | subtree:add(decoded:raw()) 46 | subtree:append_text(" (decrypted)") 47 | local tvb = ByteArray.tvb(decoded, "JSON TVB") 48 | Dissector.get("json"):call(tvb, pkt, root) 49 | end 50 | 51 | function hs1x0_proto_UDP.dissector (buf, pkt, root) 52 | pkt.cols.protocol = "TPLink-SmartHome (UDP)" 53 | local subtree = root:add(hs1x0_proto_UDP, buf() ,"TPLink-SmartHome") 54 | local decoded = tpdecode(buf, 0) 55 | subtree:add(decoded:raw()) 56 | subtree:append_text(" (decrypted)") 57 | local tvb = ByteArray.tvb(decoded, "JSON TVB") 58 | Dissector.get("json"):call(tvb, pkt, root) 59 | end 60 | 61 | tcp_table = DissectorTable.get ("tcp.port") 62 | udp_table = DissectorTable.get ("udp.port") 63 | 64 | -- register the protocol to port 9999 65 | tcp_table:add (9999, hs1x0_proto_TCP) 66 | udp_table:add (9999, hs1x0_proto_UDP) 67 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import dgram from 'dgram' 2 | import EventEmitter from 'events' 3 | 4 | export default class TPLSmartDevice { 5 | constructor (ip) { 6 | this.ip = ip 7 | } 8 | 9 | // Scan for lightbulbs on your network 10 | static scan (filter, broadcast = '255.255.255.255') { 11 | const emitter = new EventEmitter() 12 | const client = dgram.createSocket({ 13 | type: 'udp4', 14 | reuseAddr: true 15 | }) 16 | client.bind(9998, undefined, () => { 17 | client.setBroadcast(true) 18 | const msgBuf = TPLSmartDevice.encrypt(Buffer.from('{"system":{"get_sysinfo":{}}}')) 19 | client.send(msgBuf, 0, msgBuf.length, 9999, broadcast) 20 | }) 21 | client.on('message', (msg, rinfo) => { 22 | const decryptedMsg = this.decrypt(msg).toString('ascii') 23 | const jsonMsg = JSON.parse(decryptedMsg) 24 | const sysinfo = jsonMsg.system.get_sysinfo 25 | 26 | if (filter && sysinfo.mic_type !== filter) { 27 | return 28 | } 29 | 30 | const light = new TPLSmartDevice(rinfo.address) 31 | light._info = rinfo 32 | light._sysinfo = sysinfo 33 | light.host = rinfo.address 34 | light.port = rinfo.port 35 | light.name = sysinfo.alias 36 | light.deviceId = sysinfo.deviceId 37 | 38 | emitter.emit('light', light) 39 | }) 40 | emitter.stop = () => client.close() 41 | return emitter 42 | } 43 | 44 | // Send a message to a lightbulb (for RAW JS message objects) 45 | send (msg) { 46 | return new Promise((resolve, reject) => { 47 | if (!this.ip) { 48 | return reject(new Error('IP not set.')) 49 | } 50 | const client = dgram.createSocket('udp4') 51 | const message = this.encrypt(Buffer.from(JSON.stringify(msg))) 52 | client.send(message, 0, message.length, 9999, this.ip, (err, bytes) => { 53 | if (err) { 54 | return reject(err) 55 | } 56 | client.on('message', msg => { 57 | resolve(JSON.parse(this.decrypt(msg).toString())) 58 | client.close() 59 | }) 60 | }) 61 | }) 62 | } 63 | 64 | // TODO: wifi needs more testing. it seems very broken. 65 | 66 | // Scans the wifi networks in range of the device 67 | async listwifi () { 68 | const r1 = await this.send({ 69 | netif: { 70 | get_scaninfo: { 71 | refresh: 1 72 | } 73 | } 74 | }) 75 | 76 | if (r1?.netif?.get_scaninfo?.ap_list) { 77 | return r1.netif.get_scaninfo.ap_list 78 | } else { 79 | // on fail, try older message-format 80 | const r2 = await this.send({ 81 | 'smartlife.iot.common.softaponboarding': { 82 | get_scaninfo: { 83 | refresh: 1 84 | } 85 | } 86 | }) 87 | if (r2 && r2['smartlife.iot.common.softaponboarding']?.get_scaninfo?.ap_list) { 88 | return r2['smartlife.iot.common.softaponboarding'].get_scaninfo.ap_list 89 | } 90 | } 91 | } 92 | 93 | // Connects the device to the access point in the parameters 94 | async connectwifi (ssid, password, keyType = 1, cypherType = 0) { 95 | const r1 = await this.send({ 96 | netif: { 97 | set_stainfo: { 98 | cypher_type: cypherType, 99 | key_type: keyType, 100 | password, 101 | ssid 102 | } 103 | } 104 | }) 105 | 106 | if (r1?.netif?.set_stainfo?.err_code === 0) { 107 | return true 108 | } 109 | 110 | // on fail, try older message-format 111 | 112 | const r2 = await this.send({ 113 | 'smartlife.iot.common.softaponboarding': { 114 | set_stainfo: { 115 | cypher_type: cypherType, 116 | key_type: keyType, 117 | password, 118 | ssid 119 | } 120 | } 121 | }) 122 | if (r2['smartlife.iot.common.softaponboarding'] && r2['smartlife.iot.common.softaponboarding'].err_msg) { 123 | throw new Error(r2['smartlife.iot.common.softaponboarding'].err_msg) 124 | } else { 125 | return true 126 | } 127 | } 128 | 129 | // Get info about the TPLSmartDevice 130 | async info () { 131 | const r = await this.send({ system: { get_sysinfo: {} } }) 132 | return r.system.get_sysinfo 133 | } 134 | 135 | // Set power-state of lightbulb 136 | async power (powerState = true, transition = 0, options = {}) { 137 | const info = await this.info() 138 | if (typeof info.relay_state !== 'undefined') { 139 | return this.send({ 140 | system: { 141 | set_relay_state: { 142 | state: powerState ? 1 : 0 143 | } 144 | } 145 | }) 146 | } else { 147 | const r = await this.send({ 148 | 'smartlife.iot.smartbulb.lightingservice': { 149 | transition_light_state: { 150 | ignore_default: 1, 151 | on_off: powerState ? 1 : 0, 152 | transition_period: transition, 153 | ...options 154 | } 155 | } 156 | }) 157 | return r['smartlife.iot.smartbulb.lightingservice'].transition_light_state 158 | } 159 | } 160 | 161 | // Set led-state of lightbulb 162 | led (ledState = true) { 163 | return this.send({ system: { set_led_off: { off: ledState ? 0 : 1 } } }) 164 | } 165 | 166 | // Set the name of lightbulb 167 | async setName (newAlias) { 168 | const info = await this.info() 169 | return typeof info.dev_name !== 'undefined' 170 | ? this.send({ system: { set_dev_alias: { alias: newAlias } } }) 171 | : this.send({ 'smartlife.iot.common.system': { set_dev_alias: { alias: newAlias } } }) 172 | } 173 | 174 | // Get schedule info 175 | async daystat (month, year) { 176 | const now = new Date() 177 | month = month || now.getMonth() + 1 178 | year = year || now.getFullYear() 179 | const r = await this.send({ 'smartlife.iot.common.schedule': { get_daystat: { month: month, year: year } } }) 180 | return r['smartlife.iot.common.schedule'].get_daystat 181 | } 182 | 183 | // Get cloud info from bulb 184 | async cloud () { 185 | const r = await this.send({ 'smartlife.iot.common.cloud': { get_info: {} } }) 186 | return r['smartlife.iot.common.cloud'].get_info 187 | } 188 | 189 | // Get schedule from bulb 190 | async schedule () { 191 | const r = await this.send({ 'smartlife.iot.common.schedule': { get_rules: {} } }) 192 | return r['smartlife.iot.common.schedule'].get_rules 193 | } 194 | 195 | // Get operational details from bulb 196 | details () { 197 | return this.send({ 'smartlife.iot.smartbulb.lightingservice': { get_light_details: {} } }) 198 | } 199 | 200 | // Reboot the device 201 | reboot () { 202 | return this.send({ 'smartlife.iot.common.system': { reboot: { delay: 1 } } }) 203 | } 204 | 205 | // Badly encrypt message in format bulbs use 206 | static encrypt (buffer, key = 0xAB) { 207 | for (let i = 0; i < buffer.length; i++) { 208 | const c = buffer[i] 209 | buffer[i] = c ^ key 210 | key = buffer[i] 211 | } 212 | return buffer 213 | } 214 | 215 | encrypt (buffer, key) { 216 | return TPLSmartDevice.encrypt(buffer, key) 217 | } 218 | 219 | // Badly decrypt message from format bulbs use 220 | static decrypt (buffer, key = 0xAB) { 221 | for (let i = 0; i < buffer.length; i++) { 222 | const c = buffer[i] 223 | buffer[i] = c ^ key 224 | key = c 225 | } 226 | return buffer 227 | } 228 | 229 | decrypt (buffer, key) { 230 | return TPLSmartDevice.decrypt(buffer, key) 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | tested with LB120 2 | 3 | # tplink-lightbulb 4 | 5 | Control TP-Link smart-home devices from nodejs 6 | 7 | [![NPM](https://badge.fury.io/js/tplink-lightbulb.svg)](https://nodei.co/npm/tplink-lightbulb/) 8 | 9 | This will allow you to control TP-Link smart-home devices from nodejs or the command-line. 10 | 11 | ## related 12 | 13 | * If you want to use kasa (allows you to hit your tplink devices, on an external network) have a look at [kasa_control](https://github.com/konsumer/kasa_control). 14 | * If you'd like to run a GraphQL server to control your lights, see [tplink-graphql](https://github.com/konsumer/tplink-graphql). 15 | * If you like to see a demo web-app that uses react & websockets, see [tpserver](https://github.com/konsumer/tpserver). 16 | 17 | ## supported devices 18 | 19 | Not all TP-Link smart-home devices can do all things, here's the support-matrix: 20 | 21 | | | raw | details | on | off | temp | hex | hsb | cloud | wifi | join | 22 | |------------------------------------------------------------------------------------------------------------:|:---:|:-------:|:--:|:---:|:----:|:---:|:---:|:-----:|:----:|:----:| 23 | | [LB100](http://www.tp-link.com/us/products/details/cat-5609_LB100.html) | X | X | X | X | X | | | X | X | X | 24 | | [LB120](http://www.tp-link.com/us/products/details/cat-5609_LB120.html) | X | X | X | X | X | | | X | X | X | 25 | | [LB130](http://www.tp-link.com/us/products/details/cat-5609_LB130.html) | X | X | X | X | X | X | X | X | X | X | 26 | | [HS100](http://www.tp-link.com/us/products/details/cat-5516_HS100.html) | X | X | X | X | | | | | X | X | 27 | | [HS105](http://www.tp-link.com/us/products/details/cat-5516_HS105.html) | X | X | X | X | | | | | X | X | 28 | | [HS110](http://www.tp-link.com/us/products/details/cat-5516_HS110.html) | X | X | X | X | | | | | X | X | 29 | | [HS200](http://www.tp-link.com/us/products/details/cat-5622_HS200.html) | X | X | X | X | | | | | X | X | 30 | | [KP100](http://www.tp-link.com/us/products/details/cat-5516_KP100.html) | X | X | X | X | | | | | X | X | 31 | | [LB200](http://www.tp-link.com/us/products/details/cat-5609_LB200.html) | X | X | X | X | X | | | X | X | X | 32 | | [LB230](http://www.tp-link.com/us/products/details/cat-5609_LB230.html) | X | X | X | X | X | X | X | X | X | X | 33 | | [KL110](https://www.tp-link.com/uk/home-networking/smart-bulb/kl110/) | X | X | X | X | | | | | X | X | 34 | | [KL120](https://www.tp-link.com/uk/home-networking/smart-bulb/kl120/) | X | X | X | X | X | | | X | X | X | 35 | | [KL130](https://www.kasasmart.com/us/products/smart-lighting/kasa-smart-wi-fi-light-bulb-multicolor-kl130/) | X | X | X | X | X | X | X | X | X | X | 36 | 37 | I have LB120, LB130, and HS105, so any testing (and packet-capture) with other devices would be greatly appreciated. 38 | 39 | 40 | ## command-line 41 | 42 | If you have nodejs installed, you can install it for your system with this: 43 | 44 | ``` 45 | npm i -g tplink-lightbulb 46 | ``` 47 | 48 | You can even run it without installing: 49 | 50 | ``` 51 | npx tplink-lightbulb 52 | ``` 53 | 54 | If you don't want to install nodejs, or just want the standalone-version, install a [release](https://github.com/konsumer/tplink-lightbulb/releases) for your system. 55 | 56 | Now, you can use it like this: 57 | 58 | ``` 59 | Usage: tplight 60 | 61 | Commands: 62 | tplight scan Scan for lightbulbs 63 | tplight on Turn on lightbulb 64 | tplight off Turn off lightbulb 65 | tplight bright Set the brightness of the lightbulb 66 | (for those that support it) 67 | tplight temp Set the color-temperature of the 68 | lightbulb (for those that support 69 | it) 70 | tplight hex Set color of lightbulb using hex 71 | color (for those that support it) 72 | tplight hsb Set color of lightbulb using HSB 73 | color (for those that support it) 74 | tplight cloud Get cloud info 75 | tplight raw Send a raw JSON command 76 | tplight details Get details about the device 77 | tplight led Turn on/off LED indicator 78 | tplight wifi List available wifi for a particular 79 | device 80 | tplight join [SECRET] Configure the device to use these 81 | wifi settings 82 | 83 | Options: 84 | -h, --help Show help [boolean] 85 | --version Show version number [boolean] 86 | 87 | Examples: 88 | tplight scan -h Get more detailed help with `scan` command 89 | tplight on -h Get more detailed help with `on` command 90 | tplight off -h Get more detailed help with `off` command 91 | tplight temp -h Get more detailed help with `temp` command 92 | tplight hex -h Get more detailed help with `hex` command 93 | tplight hsb -h Get more detailed help with `hsb` command 94 | tplight cloud -h Get more detailed help with `cloud` command 95 | tplight raw -h Get more detailed help with `raw` command 96 | tplight details -h Get more detailed help with `details` command 97 | tplight led -h Get more detailed help with `led` command 98 | tplight wifi -h Get more detailed help with `wifi` command 99 | tplight join -h Get more detailed help with `join` command 100 | ``` 101 | 102 | ## wireshark 103 | 104 | If you want to analyze the protocol, you can use the included `tplink-smarthome.lua`. 105 | 106 | Install in the location listed in About Wireshark/Folders/Personal Plugins 107 | 108 | I captured packets with tcpdump running on a [raspberry pi pretending to be a router](https://learn.adafruit.com/setting-up-a-raspberry-pi-as-a-wifi-access-point?view=all). In general, this is a really useful way to capture IOT protocols and mess around with them. 109 | 110 | I ssh'd into my pi, ran `sudo apt update && sudo apt install tcpdump`, then `tcpdump -i wlan0 -w lights.pcap` 111 | 112 | I connected the lights to that network (reset them to factory default by turning the power off/on 5 times, then configure in Kasa app.) 113 | 114 | After I did stuff like switch the lights on/off in app, I open the pcap file in wireshark on my desktop. 115 | 116 | ## library 117 | 118 | You can install it in your project like this: 119 | 120 | ``` 121 | npm i -S tplink-lightbulb 122 | ``` 123 | 124 | Include it in your project like this: 125 | 126 | ```js 127 | const TPLSmartDevice = require('tplink-lightbulb') 128 | ``` 129 | 130 | or for ES6: 131 | 132 | ```js 133 | import TPLSmartDevice from 'tplink-lightbulb' 134 | ``` 135 | 136 | Read more about [the API](https://github.com/konsumer/tplink-lightbulb/blob/master/API.md). 137 | -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | # [tplink-lightbulb](https://github.com/konsumer/tplink-lightbulb#readme) *1.8.0* 2 | 3 | > Control TP-Link smart-home devices from nodejs 4 | 5 | 6 | ### src/lib.js 7 | 8 | 9 | #### scan(filter, broadcast) 10 | 11 | Scan for lightbulbs on your network 12 | 13 | 14 | 15 | 16 | ##### Parameters 17 | 18 | | Name | Type | Description | | 19 | | ---- | ---- | ----------- | -------- | 20 | | filter | `string` | [none] Only return devices with this class, (ie 'IOT.SMARTBULB') |   | 21 | | broadcast | `string` | ['255.255.255.255'] Use this broadcast IP |   | 22 | 23 | 24 | 25 | 26 | ##### Examples 27 | 28 | ```javascript 29 | // turn first discovered light off 30 | const scan = TPLSmartDevice.scan() 31 | .on('light', light => { 32 | light.power(false) 33 | .then(status => { 34 | console.log(status) 35 | scan.stop() 36 | }) 37 | }) 38 | ``` 39 | 40 | 41 | ##### Returns 42 | 43 | 44 | - `EventEmitter` Emit `light` events when lightbulbs are found 45 | 46 | 47 | 48 | #### listwifi() 49 | 50 | Scans the wifi networks in range of the device 51 | 52 | 53 | 54 | 55 | 56 | 57 | ##### Examples 58 | 59 | ```javascript 60 | // scan for available wifi 61 | const light = new TPLSmartDevice('10.0.0.200') 62 | light.listwifi() 63 | .then(info => { 64 | console.log(info) 65 | }) 66 | ``` 67 | 68 | 69 | ##### Returns 70 | 71 | 72 | - `Promise` Resolves to output of command 73 | 74 | 75 | 76 | #### connectwifi(ssid, password, keyType, cypherType) 77 | 78 | Connects the device to the access point in the parameters 79 | 80 | 81 | 82 | 83 | ##### Parameters 84 | 85 | | Name | Type | Description | | 86 | | ---- | ---- | ----------- | -------- | 87 | | ssid | | {String} Your wifi SSID |   | 88 | | password | | {String} Your wifi secret |   | 89 | | keyType | | {Number} The type of key (WPA 2 is 3, no key is 0) |   | 90 | | cypherType | | {Number} The type of cypher (WPA2 is 2) |   | 91 | 92 | 93 | 94 | 95 | ##### Examples 96 | 97 | ```javascript 98 | // command a device to join a wifi network 99 | const light = new TPLSmartDevice('10.0.0.200') 100 | light.connectwifi("SSID", "PASSWORD", 3, 2) 101 | .then(info => { 102 | console.log(info) 103 | }) 104 | ``` 105 | 106 | 107 | ##### Returns 108 | 109 | 110 | - `Promise` Resolves to output of command 111 | 112 | 113 | 114 | #### info() 115 | 116 | Get info about the TPLSmartDevice 117 | 118 | 119 | 120 | 121 | 122 | 123 | ##### Examples 124 | 125 | ```javascript 126 | // get info about a light 127 | const light = new TPLSmartDevice('10.0.0.200') 128 | light.info() 129 | .then(info => { 130 | console.log(info) 131 | }) 132 | ``` 133 | 134 | 135 | ##### Returns 136 | 137 | 138 | - `Promise` Resolves to info 139 | 140 | 141 | 142 | #### send(msg) 143 | 144 | Send a message to a lightbulb (for RAW JS message objects) 145 | 146 | 147 | 148 | 149 | ##### Parameters 150 | 151 | | Name | Type | Description | | 152 | | ---- | ---- | ----------- | -------- | 153 | | msg | `Object` | Message to send to bulb |   | 154 | 155 | 156 | 157 | 158 | ##### Examples 159 | 160 | ```javascript 161 | const light = new TPLSmartDevice('10.0.0.200') 162 | light.send({ 163 | 'smartlife.iot.smartbulb.lightingservice': { 164 | 'transition_light_state': { 165 | 'on_off': 1, 166 | 'transition_period': 0 167 | } 168 | }}) 169 | .then(response => { 170 | console.log(response) 171 | }) 172 | .catch(e => console.error(e)) 173 | ``` 174 | 175 | 176 | ##### Returns 177 | 178 | 179 | - `Promise` Resolves with answer 180 | 181 | 182 | 183 | #### power(powerState, transition, options) 184 | 185 | Set power-state of lightbulb 186 | 187 | 188 | 189 | 190 | ##### Parameters 191 | 192 | | Name | Type | Description | | 193 | | ---- | ---- | ----------- | -------- | 194 | | powerState | `Boolean` | On or off |   | 195 | | transition | `Number` | Transition to new state in this time |   | 196 | | options | `Object` | Object containing `mode`, `hue`, `saturation`, `color_temp`, `brightness` |   | 197 | 198 | 199 | 200 | 201 | ##### Examples 202 | 203 | ```javascript 204 | // turn a light on 205 | const light = new TPLSmartDevice('10.0.0.200') 206 | light.power(true) 207 | .then(status => { 208 | console.log(status) 209 | }) 210 | .catch(err => console.error(err)) 211 | ``` 212 | 213 | 214 | ##### Returns 215 | 216 | 217 | - `Promise` Resolves to output of command 218 | 219 | 220 | 221 | #### led(ledState) 222 | 223 | Set led-state of lightbulb 224 | 225 | 226 | 227 | 228 | ##### Parameters 229 | 230 | | Name | Type | Description | | 231 | | ---- | ---- | ----------- | -------- | 232 | | ledState | `Boolean` | On or off |   | 233 | 234 | 235 | 236 | 237 | ##### Examples 238 | 239 | ```javascript 240 | // turn the LED status light on 241 | const light = new TPLSmartDevice('10.0.0.200') 242 | light.led(true) 243 | .then(status => { 244 | console.log(status) 245 | }) 246 | .catch(err => console.error(err)) 247 | ``` 248 | 249 | 250 | ##### Returns 251 | 252 | 253 | - `Promise` Resolves to output of command 254 | 255 | 256 | 257 | #### setName(newAlias) 258 | 259 | Set the name of lightbulb 260 | 261 | 262 | 263 | 264 | ##### Parameters 265 | 266 | | Name | Type | Description | | 267 | | ---- | ---- | ----------- | -------- | 268 | | newAlias | `String` | |   | 269 | 270 | 271 | 272 | 273 | ##### Examples 274 | 275 | ```javascript 276 | // change the name of a light 277 | const light = new TPLSmartDevice('10.0.0.200') 278 | light.setName("New Name") 279 | .then(status => { 280 | console.log(status) 281 | }) 282 | .catch(err => console.error(err)) 283 | ``` 284 | 285 | 286 | ##### Returns 287 | 288 | 289 | - `Promise` Resolves to output of command 290 | 291 | 292 | 293 | #### daystat(month, year) 294 | 295 | Get schedule info 296 | 297 | 298 | 299 | 300 | ##### Parameters 301 | 302 | | Name | Type | Description | | 303 | | ---- | ---- | ----------- | -------- | 304 | | month | `Number` | Month to check: 1-12 |   | 305 | | year | `Number` | Full year to check: ie 2017 |   | 306 | 307 | 308 | 309 | 310 | ##### Examples 311 | 312 | ```javascript 313 | // get the light's schedule for 1/2017 314 | const light = new TPLSmartDevice('10.0.0.200') 315 | light.schedule(1, 2017) 316 | .then(schedule => { 317 | console.log(schedule) 318 | }) 319 | .catch(e => console.error(e)) 320 | ``` 321 | 322 | 323 | ##### Returns 324 | 325 | 326 | - `Promise` Resolves to schedule info 327 | 328 | 329 | 330 | #### cloud() 331 | 332 | Get cloud info from bulb 333 | 334 | 335 | 336 | 337 | 338 | 339 | ##### Examples 340 | 341 | ```javascript 342 | // get the cloud info for the light 343 | const light = new TPLSmartDevice('10.0.0.200') 344 | light.cloud() 345 | .then(info => { 346 | console.log(info) 347 | }) 348 | .catch(e => console.error(e)) 349 | ``` 350 | 351 | 352 | ##### Returns 353 | 354 | 355 | - `Promise` Resolves to cloud info 356 | 357 | 358 | 359 | #### schedule() 360 | 361 | Get schedule from bulb 362 | 363 | 364 | 365 | 366 | 367 | 368 | ##### Examples 369 | 370 | ```javascript 371 | // get the bulb's schedule 372 | const light = new TPLSmartDevice('10.0.0.200') 373 | light.schedule() 374 | .then(schedule => { 375 | console.log(schedule) 376 | }) 377 | .catch(e => console.error(e)) 378 | ``` 379 | 380 | 381 | ##### Returns 382 | 383 | 384 | - `Promise` Resolves to schedule info 385 | 386 | 387 | 388 | #### details() 389 | 390 | Get operational details from bulb 391 | 392 | 393 | 394 | 395 | 396 | 397 | ##### Examples 398 | 399 | ```javascript 400 | // get some extra details about the light 401 | const light = new TPLSmartDevice('10.0.0.200') 402 | light.details() 403 | .then(details => { 404 | console.log(details) 405 | }) 406 | .catch(e => console.error(e)) 407 | ``` 408 | 409 | 410 | ##### Returns 411 | 412 | 413 | - `Promise` Resolves to operational details 414 | 415 | 416 | 417 | #### reboot() 418 | 419 | Reboot the device 420 | 421 | 422 | 423 | 424 | 425 | 426 | ##### Examples 427 | 428 | ```javascript 429 | // get some extra details about the light 430 | const light = new TPLSmartDevice('10.0.0.200') 431 | light.reboot() 432 | .then(status => { 433 | console.log(status) 434 | }) 435 | .catch(e => console.error(e)) 436 | ``` 437 | 438 | 439 | ##### Returns 440 | 441 | 442 | - `Promise` Resolves to output of command 443 | 444 | 445 | 446 | #### encrypt(buffer, key) 447 | 448 | Badly encrypt message in format bulbs use 449 | 450 | 451 | 452 | 453 | ##### Parameters 454 | 455 | | Name | Type | Description | | 456 | | ---- | ---- | ----------- | -------- | 457 | | buffer | `Buffer` | Buffer of data to encrypt |   | 458 | | key | `Number` | Encryption key (default is generally correct) |   | 459 | 460 | 461 | 462 | 463 | ##### Examples 464 | 465 | ```javascript 466 | const encrypted = TPLSmartDevice.encrypt(Buffer.from('super secret text')) 467 | ``` 468 | 469 | 470 | ##### Returns 471 | 472 | 473 | - `Buffer` Encrypted data 474 | 475 | 476 | 477 | #### decrypt(buffer, key) 478 | 479 | Badly decrypt message from format bulbs use 480 | 481 | 482 | 483 | 484 | ##### Parameters 485 | 486 | | Name | Type | Description | | 487 | | ---- | ---- | ----------- | -------- | 488 | | buffer | `Buffer` | Buffer of data to decrypt |   | 489 | | key | `Number` | Encryption key (default is generally correct) |   | 490 | 491 | 492 | 493 | 494 | ##### Examples 495 | 496 | ```javascript 497 | const decrypted = TPLSmartDevice.decrypt(encrypted) 498 | ``` 499 | 500 | 501 | ##### Returns 502 | 503 | 504 | - `Buffer` Decrypted data 505 | 506 | 507 | 508 | 509 | *Documentation generated with [doxdox](https://github.com/neogeek/doxdox).* 510 | -------------------------------------------------------------------------------- /src/tplight.cjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const TPLSmartDevice = require('../dist/tplink-lightbulb.cjs') 4 | const yargs = require('yargs') 5 | 6 | // https://gist.github.com/xenozauros/f6e185c8de2a04cdfecf 7 | function hexToHsl (hex) { 8 | const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex) 9 | const r = parseInt(result[1], 16) / 255 10 | const g = parseInt(result[2], 16) / 255 11 | const b = parseInt(result[3], 16) / 255 12 | const max = Math.max(r, g, b); const min = Math.min(r, g, b) 13 | let h; let s; const l = (max + min) / 2 14 | if (max === min) { 15 | h = s = 0 // achromatic 16 | } else { 17 | const d = max - min 18 | s = l > 0.5 ? d / (2 - max - min) : d / (max + min) 19 | switch (max) { 20 | case r: h = (g - b) / d + (g < b ? 6 : 0); break 21 | case g: h = (b - r) / d + 2; break 22 | case b: h = (r - g) / d + 4; break 23 | } 24 | h /= 6 25 | } 26 | return { h, s, l } 27 | } 28 | 29 | async function readStream (stream = process.stdin) { 30 | const chunks = [] 31 | return new Promise((resolve, reject) => { 32 | stream.on('data', chunk => chunks.push(chunk)) 33 | stream.on('end', () => { 34 | resolve(Buffer.concat(chunks).toString('utf8')) 35 | }) 36 | }) 37 | } 38 | 39 | // wrapper that will output JSON or colored depending on how it's being piped 40 | const json = process.stdout.isTTY ? s => console.dir(s, { depth: null, colors: true, maxArrayLength: null, maxStringLength: null }) : s => console.log(JSON.stringify(s, null, 2)) 41 | 42 | // for pkg support 43 | if (typeof process.pkg !== 'undefined') { 44 | process.pkg.defaultEntrypoint = 'tplight' 45 | } 46 | 47 | function handleError (err) { 48 | console.error(err) 49 | process.exit(1) 50 | } 51 | 52 | module.exports = yargs 53 | .usage('Usage: $0 ') 54 | .help('h') 55 | .alias('h', 'help') 56 | .demandCommand(1, 1, 'You need a command.') 57 | .version() 58 | 59 | .example('$0 scan -h', 'Get more detailed help with `scan` command') 60 | .example('$0 on -h', 'Get more detailed help with `on` command') 61 | .example('$0 off -h', 'Get more detailed help with `off` command') 62 | .example('$0 temp -h', 'Get more detailed help with `temp` command') 63 | .example('$0 hex -h', 'Get more detailed help with `hex` command') 64 | .example('$0 hsb -h', 'Get more detailed help with `hsb` command') 65 | .example('$0 cloud -h', 'Get more detailed help with `cloud` command') 66 | .example('$0 raw -h', 'Get more detailed help with `raw` command') 67 | .example('$0 details -h', 'Get more detailed help with `details` command') 68 | .example('$0 led -h', 'Get more detailed help with `led` command') 69 | .example('$0 wifi -h', 'Get more detailed help with `wifi` command') 70 | .example('$0 join -h', 'Get more detailed help with `join` command') 71 | 72 | .command('scan', 'Scan for lightbulbs', yarg => { 73 | yarg 74 | .alias('timeout', 't') 75 | .nargs('timeout', 1) 76 | .describe('timeout', 'Timeout for scan (in seconds)') 77 | .default('timeout', 0) 78 | 79 | .alias('filter', 'f') 80 | .nargs('filter', 1) 81 | .describe('filter', 'filter to a specific class of devices (ie: IOT.SMARTBULB)') 82 | 83 | .alias('broadcast', 'b') 84 | .nargs('broadcast', 1) 85 | .describe('broadcast', 'The network broadcast address for scanning') 86 | .default('broadcast', '255.255.255.255') 87 | 88 | .example('$0 scan -t 1', 'Get list of TP-Link smart devices on your network, stop after 1 second') 89 | }, argv => { 90 | const scan = TPLSmartDevice.scan(argv.filter, argv.broadcast) 91 | .on('light', light => { 92 | console.log(`${light._info.address} - ${light._sysinfo.alias} - ${light._sysinfo.model}`) 93 | }) 94 | if (argv.timeout) { 95 | setTimeout(() => { scan.stop() }, argv.timeout * 1000) 96 | } else { 97 | console.log('Press Ctrl-C to stop') 98 | } 99 | }) 100 | 101 | .command('on ', 'Turn on lightbulb', yarg => { 102 | yarg 103 | .boolean('quiet') 104 | .describe('quiet', "Don't output return value of command") 105 | .alias('quiet', 'q') 106 | 107 | .alias('transition', 't') 108 | .nargs('transition', 1) 109 | .default('transition', 0) 110 | .describe('t', 'Transition time (in ms)') 111 | 112 | .alias('brightness', 'b') 113 | .nargs('brightness', 1) 114 | .default('brightness', 100) 115 | .describe('b', 'Brightness') 116 | 117 | .example('$0 on 10.0.0.200', 'Turn on a light') 118 | .example('$0 on -t 10000 10.0.0.200', 'Take 10 seconds to turn on a light') 119 | }, argv => { 120 | const bulb = new TPLSmartDevice(argv.ip) 121 | bulb.power(true, argv.transition, { brightness: argv.brightness }) 122 | .then(r => argv.quiet || json(r)) 123 | .catch(handleError) 124 | }) 125 | 126 | .command('off ', 'Turn off lightbulb', yarg => { 127 | yarg 128 | .boolean('quiet') 129 | .describe('quiet', "Don't output return value of command") 130 | .alias('quiet', 'q') 131 | 132 | .alias('transition', 't') 133 | .nargs('transition', 1) 134 | .default('transition', 0) 135 | .describe('t', 'Transition time (in ms)') 136 | 137 | .example('$0 off 10.0.0.200', 'Turn off a light') 138 | .example('$0 off -t 10000 10.0.0.200', 'Take 10 seconds to turn off a light') 139 | }, argv => { 140 | const bulb = new TPLSmartDevice(argv.ip) 141 | bulb.power(false, argv.transition) 142 | .then(r => argv.quiet || json(r)) 143 | .catch(handleError) 144 | }) 145 | 146 | .command('bright ', 'Set the brightness of the lightbulb (for those that support it)', yarg => { 147 | yarg 148 | .boolean('quiet') 149 | .describe('quiet', "Don't output return value of command") 150 | .alias('quiet', 'q') 151 | 152 | .alias('transition', 't') 153 | .nargs('transition', 1) 154 | .default('transition', 0) 155 | .describe('t', 'Transition time (in ms)') 156 | 157 | .example('$0 bright 10.0.0.200 1', 'Set brightness to very low') 158 | .example('$0 bright 10.0.0.200 100', 'Set brightness to max') 159 | }, argv => { 160 | const bulb = new TPLSmartDevice(argv.ip) 161 | bulb.power(true, argv.transition, { brightness: argv.brightness }) 162 | .then(r => argv.quiet || json(r)) 163 | .catch(handleError) 164 | }) 165 | 166 | .command('temp ', 'Set the color-temperature of the lightbulb (for those that support it)', yarg => { 167 | yarg 168 | .boolean('quiet') 169 | .describe('quiet', "Don't output return value of command") 170 | .alias('quiet', 'q') 171 | 172 | .alias('transition', 't') 173 | .nargs('transition', 1) 174 | .default('transition', 0) 175 | .describe('t', 'Transition time (in ms)') 176 | 177 | .example('$0 temp 10.0.0.200 2500', 'Set color-temp to orangish') 178 | .example('$0 temp 10.0.0.200 9000', 'Set color-temp to bluish') 179 | }, argv => { 180 | const bulb = new TPLSmartDevice(argv.ip) 181 | bulb.power(true, argv.transition, { hue: 0, saturation: 0, color_temp: argv.color }) 182 | .then(r => argv.quiet || json(r)) 183 | .catch(handleError) 184 | }) 185 | 186 | .command('hex ', 'Set color of lightbulb using hex color (for those that support it)', yarg => { 187 | yarg 188 | .boolean('quiet') 189 | .describe('quiet', "Don't output return value of command") 190 | .alias('quiet', 'q') 191 | 192 | .alias('transition', 't') 193 | .nargs('transition', 1) 194 | .default('transition', 0) 195 | .describe('t', 'Transition time (in ms)') 196 | 197 | .example('$0 hex 10.0.0.200 "#48258b"', 'Set the lightbulb to a nice shade of purple.') 198 | .example('$0 hex -t 10000 10.0.0.200 "#48258b"', 'Take 10 seconds to set the lightbulb to a nice shade of purple.') 199 | }, argv => { 200 | const color = hexToHsl(argv.color) 201 | const bulb = new TPLSmartDevice(argv.ip) 202 | bulb.power(true, argv.transition, { hue: color.h * 100, saturation: color.s * 100, brightness: color.l * 100, color_temp: 0 }) 203 | .then(r => argv.quiet || json(r)) 204 | .catch(handleError) 205 | }) 206 | 207 | .command('hsb ', 'Set color of lightbulb using HSB color (for those that support it)', yarg => { 208 | yarg 209 | .boolean('quiet') 210 | .describe('quiet', "Don't output return value of command") 211 | .alias('quiet', 'q') 212 | 213 | .alias('transition', 't') 214 | .nargs('transition', 1) 215 | .default('transition', 0) 216 | .describe('t', 'Transition time (in ms)') 217 | 218 | .example('$0 hsb 10.0.0.200 72 58 35', 'Set the lightbulb to a nice shade of purple.') 219 | .example('$0 hsb -t 10000 10.0.0.200 72 58 35', 'Take 10 seconds to set the lightbulb to a nice shade of purple.') 220 | }, argv => { 221 | const { transition, hue, saturation, brightness } = argv 222 | const bulb = new TPLSmartDevice(argv.ip) 223 | bulb.power(true, transition, { color_temp: 0, hue, saturation, brightness }) 224 | .then(r => argv.quiet || json(r)) 225 | .catch(handleError) 226 | }) 227 | 228 | .command('cloud ', 'Get cloud info', {}, argv => { 229 | const bulb = new TPLSmartDevice(argv.ip) 230 | bulb.cloud() 231 | .then(r => json(r)) 232 | .catch(handleError) 233 | }) 234 | 235 | .command('raw [json]', 'Send a raw JSON command, use param or stdin', {}, async argv => { 236 | const bulb = new TPLSmartDevice(argv.ip) 237 | process.stdin.resume() 238 | const msg = argv.json ? argv.json : await readStream() 239 | if (!msg) { 240 | console.error('For raw, you must provide JSON via param or stdin.') 241 | process.exit(1) 242 | } 243 | bulb.send(JSON.parse(msg)) 244 | .then(r => argv.quiet || json(r)) 245 | .catch(handleError) 246 | }) 247 | 248 | .command('details ', 'Get details about the device', {}, argv => { 249 | const bulb = new TPLSmartDevice(argv.ip) 250 | Promise.all([ 251 | bulb.details(), 252 | bulb.info() 253 | ]) 254 | .then(([details, info]) => { 255 | json({ ...details, ...info }) 256 | }) 257 | .catch(handleError) 258 | }) 259 | 260 | .command('led ', 'Turn on/off LED indicator', yarg => { 261 | yarg 262 | .example('$0 led 10.0.0.200 off', 'Turn off the LED') 263 | .example('$0 led 10.0.0.200 on', 'Turn on the LED') 264 | }, argv => { 265 | const bulb = new TPLSmartDevice(argv.ip) 266 | const ledState = ['y', 'yes', 'true', '1', 'on'].indexOf(argv.ledState.toLowerCase()) === -1 267 | bulb.led(ledState) 268 | }) 269 | 270 | .command('wifi ', 'List available wifi for a particular device', yarg => { 271 | yarg 272 | .example('$0 wifi 10.0.0.200', 'List wifi') 273 | }, async argv => { 274 | try { 275 | const bulb = new TPLSmartDevice(argv.ip) 276 | const wifi = await bulb.listwifi() 277 | json(wifi) 278 | } catch (e) { 279 | handleError(e) 280 | } 281 | }) 282 | 283 | .command('join [SECRET]', 'Configure the device to use these wifi settings', yarg => { 284 | yarg 285 | .example('$0 join 10.0.0.200 "my SSID goes here" "my password goes here"', 'Setup wifi') 286 | }, async argv => { 287 | try { 288 | const bulb = new TPLSmartDevice(argv.ip) 289 | const wifi = await bulb.listwifi() 290 | const chosen = wifi.find(w => w.ssid === argv.SSID) 291 | if (!chosen) { 292 | handleError(`${argv.SSID} not found.`) 293 | } 294 | // sometimes data is missing cypher_type, so I guess based on key_type 295 | const status = await bulb.connectwifi(argv.SSID, argv.SECRET, chosen.key_type, chosen.key_type === 3 ? 2 : 0) 296 | 297 | if (status) { 298 | console.log(`OK, joined ${argv.SSID}.`) 299 | } else { 300 | console.log(`Could not join ${argv.SSID}.`) 301 | } 302 | } catch (e) { 303 | handleError(e) 304 | } 305 | }) 306 | 307 | .argv 308 | --------------------------------------------------------------------------------